├── .vscode └── extensions.json ├── test └── README ├── platformio.ini ├── lib └── README ├── include └── README └── src └── main.cpp /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "platformio.platformio-ide" 6 | ], 7 | "unwantedRecommendations": [ 8 | "ms-vscode.cpptools-extension-pack" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /test/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for PlatformIO Test Runner and project tests. 3 | 4 | Unit Testing is a software testing method by which individual units of 5 | source code, sets of one or more MCU program modules together with associated 6 | control data, usage procedures, and operating procedures, are tested to 7 | determine whether they are fit for use. Unit testing finds problems early 8 | in the development cycle. 9 | 10 | More information about PlatformIO Unit Testing: 11 | - https://docs.platformio.org/en/latest/advanced/unit-testing/index.html 12 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:esp32dev] 12 | platform = espressif32 13 | board = esp32dev 14 | framework = arduino 15 | lib_deps = 16 | adafruit/Adafruit GFX Library@^1.11.3 17 | adafruit/Adafruit SSD1351 library@^1.2.8 18 | -------------------------------------------------------------------------------- /lib/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link into executable file. 4 | 5 | The source code of each library should be placed in a an own separate directory 6 | ("lib/your_library_name/[here are source files]"). 7 | 8 | For example, see a structure of the following two libraries `Foo` and `Bar`: 9 | 10 | |--lib 11 | | | 12 | | |--Bar 13 | | | |--docs 14 | | | |--examples 15 | | | |--src 16 | | | |- Bar.c 17 | | | |- Bar.h 18 | | | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html 19 | | | 20 | | |--Foo 21 | | | |- Foo.c 22 | | | |- Foo.h 23 | | | 24 | | |- README --> THIS FILE 25 | | 26 | |- platformio.ini 27 | |--src 28 | |- main.c 29 | 30 | and a contents of `src/main.c`: 31 | ``` 32 | #include 33 | #include 34 | 35 | int main (void) 36 | { 37 | ... 38 | } 39 | 40 | ``` 41 | 42 | PlatformIO Library Dependency Finder will find automatically dependent 43 | libraries scanning project source files. 44 | 45 | More information about PlatformIO Library Dependency Finder 46 | - https://docs.platformio.org/page/librarymanager/ldf.html 47 | -------------------------------------------------------------------------------- /include/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project header files. 3 | 4 | A header file is a file containing C declarations and macro definitions 5 | to be shared between several project source files. You request the use of a 6 | header file in your project source file (C, C++, etc) located in `src` folder 7 | by including it, with the C preprocessing directive `#include'. 8 | 9 | ```src/main.c 10 | 11 | #include "header.h" 12 | 13 | int main (void) 14 | { 15 | ... 16 | } 17 | ``` 18 | 19 | Including a header file produces the same results as copying the header file 20 | into each source file that needs it. Such copying would be time-consuming 21 | and error-prone. With a header file, the related declarations appear 22 | in only one place. If they need to be changed, they can be changed in one 23 | place, and programs that include the header file will automatically use the 24 | new version when next recompiled. The header file eliminates the labor of 25 | finding and changing all the copies as well as the risk that a failure to 26 | find one copy will result in inconsistencies within a program. 27 | 28 | In C, the usual convention is to give header files names that end with `.h'. 29 | It is most portable to use only letters, digits, dashes, and underscores in 30 | header file names, and at most one dot. 31 | 32 | Read more about using header files in official GCC documentation: 33 | 34 | * Include Syntax 35 | * Include Operation 36 | * Once-Only Headers 37 | * Computed Includes 38 | 39 | https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html 40 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | //-------------------------------------------------------------------------- 2 | // 2018 Modified by Laurent Moll for Uncanny Eyes costume 3 | // https://www.hackster.io/projects/376a13/ 4 | // Based on Adafruit code - Adafruit header below: 5 | // 6 | // Uncanny eyes for PJRC Teensy 3.1 with Adafruit 1.5" OLED (product #1431) 7 | // or 1.44" TFT LCD (#2088). This uses Teensy-3.1-specific features and 8 | // WILL NOT work on normal Arduino or other boards! Use 72 MHz (Optimized) 9 | // board speed -- OLED does not work at 96 MHz. 10 | // 11 | // Adafruit invests time and resources providing this open source code, 12 | // please support Adafruit and open-source hardware by purchasing products 13 | // from Adafruit! 14 | // 15 | // Written by Phil Burgess / Paint Your Dragon for Adafruit Industries. 16 | // MIT license. SPI FIFO insight from Paul Stoffregen's ILI9341_t3 library. 17 | // Inspired by David Boccabella's (Marcwolf) hybrid servo/OLED eye concept. 18 | //-------------------------------------------------------------------------- 19 | 20 | #include 21 | #include // Core graphics lib for Adafruit displays 22 | #include // Needed for 2nd serial port on ESP32 23 | 24 | // Slightly modified headers in the eye include files to be able to include 2 and switch between them 25 | // The ESP32 has a lot of memory, so this works fine 26 | #include "defaultEye.h" // Standard human-ish hazel eye 27 | #include "newtEye.h" // Eye of newt 28 | 29 | const uint16_t (*sclera)[SCLERA_WIDTH] = scleraDefault; 30 | const uint8_t (*upper)[SCREEN_WIDTH] = upperDefault; 31 | const uint8_t (*lower)[SCREEN_WIDTH] = lowerDefault; 32 | const uint16_t (*polar)[80] = polarDefault; 33 | const uint16_t (*iris)[IRIS_MAP_WIDTH] = irisDefault; 34 | 35 | // DISPLAY HARDWARE CONFIG ------------------------------------------------- 36 | 37 | #include // OLED display library -OR- 38 | 39 | typedef Adafruit_SSD1351 displayType; // Using OLED display(s) 40 | 41 | #define DISPLAY_DC 16 // Data/command pin for BOTH displays 42 | #define DISPLAY_RESET 5 // Reset pin for BOTH displays 43 | #define SELECT_L_PIN 17 // LEFT eye chip select pin 44 | #define SELECT_R_PIN 04 // RIGHT eye chip select pin 45 | #define UART_RX_PIN 13 // Pin to receive UART commands from controller 46 | 47 | // INPUT CONFIG (for eye motion -- enable or comment out as needed) -------- 48 | 49 | #define TRACKING // If enabled, eyelid tracks pupil 50 | #define IRIS_SMOOTH // If enabled, filter input from IRIS_PIN 51 | #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 52 | #define IRIS_MAX 400 // Clip upper " (WAS: 720) - Reduced range so that it doesn't look to odd with multiple eye pairs 53 | #define AUTOBLINK // If enabled, eyes blink autonomously 54 | 55 | // Probably don't need to edit any config below this line, ----------------- 56 | // unless building a single-eye project (pendant, etc.), in which case one 57 | // of the two elements in the eye[] array further down can be commented out. 58 | 59 | // Eye blinks are a tiny 3-state machine. Per-eye allows winks + blinks. 60 | #define NOBLINK 0 // Not currently engaged in a blink 61 | #define ENBLINK 1 // Eyelid is currently closing 62 | #define DEBLINK 2 // Eyelid is currently opening 63 | typedef struct { 64 | uint8_t state; // NOBLINK/ENBLINK/DEBLINK 65 | uint32_t duration; // Duration of blink state (micros) 66 | uint32_t startTime; // Time (micros) of last state change 67 | } eyeBlink; 68 | 69 | #define MOSI_PIN 23 70 | #define SCLK_PIN 18 71 | 72 | SPISettings settings(16000000, MSBFIRST, SPI_MODE3); // 26.667MHz seems reliable on the ESP32. 73 | struct { 74 | displayType display; // OLED/TFT object 75 | uint8_t cs; // Chip select pin 76 | eyeBlink blink; // Current blink state 77 | } eye[] = { // OK to comment out one of these for single-eye display: 78 | // displayType(SELECT_L_PIN,DISPLAY_DC,0),SELECT_L_PIN,{NOBLINK}, 79 | Adafruit_SSD1351(128, 128, &SPI, SELECT_L_PIN, DISPLAY_DC, DISPLAY_RESET), SELECT_L_PIN,{NOBLINK}, 80 | Adafruit_SSD1351(128, 128, &SPI, SELECT_R_PIN, DISPLAY_DC, DISPLAY_RESET), SELECT_L_PIN,{NOBLINK}, 81 | // displayType(SELECT_R_PIN,DISPLAY_DC,0),SELECT_R_PIN,{NOBLINK}, 82 | }; 83 | #define NUM_EYES (sizeof(eye) / sizeof(eye[0])) 84 | 85 | // INITIALIZATION -- runs once at startup ---------------------------------- 86 | 87 | HardwareSerial SerialIn(1); 88 | 89 | void setup(void) { 90 | uint8_t e; 91 | 92 | Serial.begin(921600); 93 | // SerialIn.begin(9600, SERIAL_8N1, UART_RX_PIN); // disabled 94 | randomSeed(analogRead(A3)); // Seed random() from floating analog input 95 | 96 | // Both displays share a common reset line; 0 is passed to display 97 | // constructor (so no reset in begin()) -- must reset manually here: 98 | pinMode(DISPLAY_RESET, OUTPUT); 99 | digitalWrite(DISPLAY_RESET, LOW); delay(1); 100 | digitalWrite(DISPLAY_RESET, HIGH); delay(50); 101 | 102 | for(e=0; e= IRIS_HEIGHT) || 152 | (irisX < 0) || (irisX >= IRIS_WIDTH)) { // In sclera 153 | p = sclera[scleraY][scleraX]; 154 | } else { // Maybe iris... 155 | p = polar[irisY][irisX]; // Polar angle/dist 156 | d = (iScale * (p & 0x7F)) / 128; // Distance (Y) 157 | if(d < IRIS_MAP_HEIGHT) { // Within iris area 158 | a = (IRIS_MAP_WIDTH * (p >> 7)) / 512; // Angle (X) 159 | p = iris[d][a]; // Pixel = iris 160 | } else { // Not in iris 161 | p = sclera[scleraY][scleraX]; // Pixel = sclera 162 | } 163 | } 164 | pBurst[burstIdx++] = p; 165 | } 166 | } 167 | 168 | eye[e].display.startWrite(); 169 | eye[e].display.writeCommand(SSD1351_CMD_SETROW); // Y range 170 | eye[e].display.write16(0x0); 171 | eye[e].display.write16(SCREEN_HEIGHT - 1); 172 | eye[e].display.writeCommand(SSD1351_CMD_SETCOLUMN); // X range 173 | eye[e].display.write16(0x0); 174 | eye[e].display.write16(SCREEN_WIDTH - 1); 175 | eye[e].display.writeCommand(SSD1351_CMD_WRITERAM); // Begin write 176 | 177 | // For ESP32, use writePixels function to transfer the whole framebuffer in one large burst 178 | SPI.writePixels((uint8_t*)pBurst, sizeof(pBurst)); 179 | eye[e].display.endWrite(); 180 | } 181 | 182 | 183 | // EYE ANIMATION ----------------------------------------------------------- 184 | 185 | const uint8_t ease[] = { // Ease in/out curve for eye movements 3*t^2-2*t^3 186 | 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 3, // T 187 | 3, 3, 4, 4, 4, 5, 5, 6, 6, 7, 7, 8, 9, 9, 10, 10, // h 188 | 11, 12, 12, 13, 14, 15, 15, 16, 17, 18, 18, 19, 20, 21, 22, 23, // x 189 | 24, 25, 26, 27, 27, 28, 29, 30, 31, 33, 34, 35, 36, 37, 38, 39, // 2 190 | 40, 41, 42, 44, 45, 46, 47, 48, 50, 51, 52, 53, 54, 56, 57, 58, // A 191 | 60, 61, 62, 63, 65, 66, 67, 69, 70, 72, 73, 74, 76, 77, 78, 80, // l 192 | 81, 83, 84, 85, 87, 88, 90, 91, 93, 94, 96, 97, 98,100,101,103, // e 193 | 104,106,107,109,110,112,113,115,116,118,119,121,122,124,125,127, // c 194 | 128,130,131,133,134,136,137,139,140,142,143,145,146,148,149,151, // J 195 | 152,154,155,157,158,159,161,162,164,165,167,168,170,171,172,174, // a 196 | 175,177,178,179,181,182,183,185,186,188,189,190,192,193,194,195, // c 197 | 197,198,199,201,202,203,204,205,207,208,209,210,211,213,214,215, // o 198 | 216,217,218,219,220,221,222,224,225,226,227,228,228,229,230,231, // b 199 | 232,233,234,235,236,237,237,238,239,240,240,241,242,243,243,244, // s 200 | 245,245,246,246,247,248,248,249,249,250,250,251,251,251,252,252, // o 201 | 252,253,253,253,254,254,254,254,254,255,255,255,255,255,255,255 }; // n 202 | 203 | #ifdef AUTOBLINK 204 | uint32_t timeOfLastBlink = 0L, timeToNextBlink = 0L; 205 | #endif 206 | 207 | #define BCAST_ADDR 0 208 | #define SER_CMD_SIZE 9 209 | uint16_t serAddr = 1; 210 | 211 | void frame( // Process motion for a single frame of left or right eye 212 | uint16_t iScale) { // Iris scale (0-1023) passed in 213 | static uint32_t frames = 0; // Used in frame rate calculation 214 | static uint8_t eyeIndex = 0; // eye[] array counter 215 | int16_t eyeX, eyeY; 216 | uint32_t t; // Time at start of function 217 | static char serCmd[SER_CMD_SIZE+1]; 218 | static uint16_t serCmdIdx = 0; 219 | static uint16_t serNewEyeCtrl = 0; 220 | static uint16_t serEyeCtrl = 0; 221 | 222 | if(++eyeIndex >= NUM_EYES) eyeIndex = 0; // Cycle through eyes, 1 per call 223 | 224 | // X/Y movement 225 | 226 | t = micros(); 227 | 228 | // Autonomous X/Y eye motion 229 | // Periodically initiates motion to a new random point, random speed, 230 | // holds there for random period until next motion. 231 | 232 | static boolean eyeInMotion = false; 233 | static int16_t eyeOldX=512, eyeOldY=512, eyeCurX=512, eyeCurY=512, eyeNewX=512, eyeNewY=512; 234 | static uint32_t eyeMoveStartTime = 0L; 235 | static int32_t eyeMoveDuration = 0L; 236 | 237 | int32_t dt = t - eyeMoveStartTime; // uS elapsed since last eye event 238 | 239 | if(eyeInMotion) { // Currently moving? 240 | if(dt >= eyeMoveDuration) { // Time up? Destination reached. 241 | if (serEyeCtrl) { // If serial controlled, we're done moving, but stay in motion 242 | eyeX = eyeOldX = eyeNewX; // Save position 243 | eyeY = eyeOldY = eyeNewY; 244 | } else { 245 | eyeInMotion = false; // Stop moving 246 | eyeMoveDuration = random(3000000); // 0-3 sec stop 247 | eyeMoveStartTime = t; // Save initial time of stop 248 | eyeX = eyeOldX = eyeNewX; // Save position 249 | eyeY = eyeOldY = eyeNewY; 250 | } 251 | } else { // Move time's not yet fully elapsed -- interpolate position 252 | int16_t e = ease[255 * dt / eyeMoveDuration] + 1; // Ease curve 253 | eyeX = eyeOldX + (((eyeNewX - eyeOldX) * e) / 256); // Interp X 254 | eyeY = eyeOldY + (((eyeNewY - eyeOldY) * e) / 256); // and Y 255 | } 256 | } else { // Eye stopped 257 | eyeX = eyeOldX; 258 | eyeY = eyeOldY; 259 | if(dt > eyeMoveDuration) { // Time up? Begin new move. 260 | int16_t dx, dy; 261 | uint32_t d; 262 | do { // Pick new dest in circle 263 | eyeNewX = random(1024); 264 | eyeNewY = random(1024); 265 | dx = (eyeNewX * 2) - 1023; 266 | dy = (eyeNewY * 2) - 1023; 267 | } while((d = (dx * dx + dy * dy)) > (1023 * 1023)); // Keep trying 268 | eyeMoveDuration = random(72000, 144000); // ~1/14 - ~1/7 sec 269 | eyeMoveStartTime = t; // Save initial time of move 270 | eyeInMotion = true; // Start move on next frame 271 | } 272 | } 273 | eyeCurX = eyeX; 274 | eyeCurY = eyeY; 275 | 276 | // Blinking 277 | #ifdef AUTOBLINK 278 | // Similar to the autonomous eye movement above -- blink start times 279 | // and durations are random (within ranges). 280 | if((t - timeOfLastBlink) >= timeToNextBlink) { // Start new blink? 281 | timeOfLastBlink = t; 282 | uint32_t blinkDuration = random(36000, 72000); // ~1/28 - ~1/14 sec 283 | // Set up durations for both eyes (if not already winking) 284 | for(uint8_t e=0; e= eye[eyeIndex].blink.duration) { 298 | // No buttons, or other state... 299 | if(++eye[eyeIndex].blink.state > DEBLINK) { // Deblinking finished? 300 | eye[eyeIndex].blink.state = NOBLINK; // No longer blinking 301 | } else { // Advancing from ENBLINK to DEBLINK mode 302 | eye[eyeIndex].blink.duration *= 2; // DEBLINK is 1/2 ENBLINK speed 303 | eye[eyeIndex].blink.startTime = t; 304 | } 305 | 306 | } 307 | } 308 | 309 | // Process motion, blinking and iris scale into renderable values 310 | 311 | // Iris scaling: remap from 0-1023 input to iris map height pixel units 312 | iScale = ((IRIS_MAP_HEIGHT + 1) * 1024) / 313 | (1024 - (iScale * (IRIS_MAP_HEIGHT - 1) / IRIS_MAP_HEIGHT)); 314 | 315 | // Scale eye X/Y positions (0-1023) to pixel units used by drawEye() 316 | eyeX = map(eyeX, 0, 1023, 0, SCLERA_WIDTH - 128); 317 | eyeY = map(eyeY, 0, 1023, 0, SCLERA_HEIGHT - 128); 318 | if (eyeIndex == 1) { // this inverts the motion of the eyes 319 | // eyeX = (SCLERA_WIDTH - 128) - eyeX; // Mirrored display 320 | } 321 | 322 | // Horizontal position is offset so that eyes are very slightly crossed 323 | // to appear fixated (converged) at a conversational distance. Number 324 | // here was extracted from my posterior and not mathematically based. 325 | // I suppose one could get all clever with a range sensor, but for now... 326 | eyeX += 4; 327 | if(eyeX > (SCLERA_WIDTH - 128)) eyeX = (SCLERA_WIDTH - 128); 328 | 329 | // Eyelids are rendered using a brightness threshold image. This same 330 | // map can be used to simplify another problem: making the upper eyelid 331 | // track the pupil (eyes tend to open only as much as needed -- e.g. look 332 | // down and the upper eyelid drops). Just sample a point in the upper 333 | // lid map slightly above the pupil to determine the rendering threshold. 334 | static uint8_t uThreshold = 128; 335 | uint8_t lThreshold, n; 336 | #ifdef TRACKING 337 | int16_t sampleX = SCLERA_WIDTH / 2 - (eyeX / 2), // Reduce X influence 338 | sampleY = SCLERA_HEIGHT / 2 - (eyeY + IRIS_HEIGHT / 4); 339 | // Eyelid is slightly asymmetrical, so two readings are taken, averaged 340 | if(sampleY < 0) n = 0; 341 | else n = (upper[sampleY][sampleX] + 342 | upper[sampleY][SCREEN_WIDTH - 1 - sampleX]) / 2; 343 | uThreshold = (uThreshold * 3 + n) / 4; // Filter/soften motion 344 | // Lower eyelid doesn't track the same way, but seems to be pulled upward 345 | // by tension from the upper lid. 346 | lThreshold = 254 - uThreshold; 347 | #else // No tracking -- eyelids full open unless blink modifies them 348 | uThreshold = lThreshold = 0; 349 | #endif 350 | 351 | // The upper/lower thresholds are then scaled relative to the current 352 | // blink position so that blinks work together with pupil tracking. 353 | if(eye[eyeIndex].blink.state) { // Eye currently blinking? 354 | uint32_t s = (t - eye[eyeIndex].blink.startTime); 355 | if(s >= eye[eyeIndex].blink.duration) s = 255; // At or past blink end 356 | else s = 255 * s / eye[eyeIndex].blink.duration; // Mid-blink 357 | s = (eye[eyeIndex].blink.state == DEBLINK) ? 1 + s : 256 - s; 358 | n = (uThreshold * s + 254 * (257 - s)) / 256; 359 | lThreshold = (lThreshold * s + 254 * (257 - s)) / 256; 360 | } else { 361 | n = uThreshold; 362 | } 363 | 364 | // Pass all the derived values to the eye-rendering function: 365 | drawEye(eyeIndex, iScale, eyeX, eyeY, n, lThreshold); 366 | } 367 | 368 | 369 | // AUTONOMOUS IRIS SCALING (if no photocell or dial) ----------------------- 370 | // Autonomous iris motion uses a fractal behavior to similate both the major 371 | // reaction of the eye plus the continuous smaller adjustments that occur. 372 | 373 | uint16_t oldIris = (IRIS_MIN + IRIS_MAX) / 2, newIris; 374 | 375 | void split( // Subdivides motion path into two sub-paths w/randomization 376 | int16_t startValue, // Iris scale value (IRIS_MIN to IRIS_MAX) at start 377 | int16_t endValue, // Iris scale value at end 378 | uint32_t startTime, // micros() at start 379 | int32_t duration, // Start-to-end time, in microseconds 380 | int16_t range) { // Allowable scale value variance when subdividing 381 | 382 | if(range >= 8) { // Limit subdvision count, because recursion 383 | range /= 2; // Split range & time in half for subdivision, 384 | duration /= 2; // then pick random center point within range: 385 | int16_t midValue = (startValue + endValue - range) / 2 + random(range); 386 | uint32_t midTime = startTime + duration; 387 | split(startValue, midValue, startTime, duration, range); // First half 388 | split(midValue , endValue, midTime , duration, range); // Second half 389 | } else { // No more subdivisons, do iris motion... 390 | int32_t dt; // Time (micros) since start of motion 391 | int16_t v; // Interim value 392 | while((dt = (micros() - startTime)) < duration) { 393 | v = startValue + (((endValue - startValue) * dt) / duration); 394 | if(v < IRIS_MIN) v = IRIS_MIN; // Clip just in case 395 | else if(v > IRIS_MAX) v = IRIS_MAX; 396 | frame(v); // Draw frame w/interim iris scale value 397 | } 398 | } 399 | } 400 | 401 | // MAIN LOOP -- runs continuously after setup() ---------------------------- 402 | 403 | void loop() { 404 | 405 | // Autonomous iris scaling -- invoke recursive function 406 | 407 | newIris = random(IRIS_MIN, IRIS_MAX); 408 | 409 | split(oldIris, newIris, micros(), 10000000L, IRIS_MAX - IRIS_MIN); 410 | oldIris = newIris; 411 | } --------------------------------------------------------------------------------