Not understanding touchRead

jrraines

Well-known member
Last year I did something similar to https://learn.adafruit.com/animated-electronic-eyes-using-teensy-3-1/bowler-hat. Halloween is coming and I was going to play with this concept some more. A simple modification would be to toggle the display from the realistic eye to a cartoon spiral, controlled by touching something. I could poke a wire all the way through the hat but having a wire at the top of the hat that doesn't actually poke through seems like a more satisfying design. Capacitative sense might be able to do this. The display will be close to my head inside the hat but the teensy can be near the top of the hat. Duck tape with a backing so the glue doesn't get on the boards can be used inside the hat.

In the field, plugging a battery in starts the program running. I'm thinking I need a baseline reading of touchRead while the hat is on my head (which would be say 2-3 inches from the teensy and the touch wire. When I touch the top of the hat the display would toggle.

So I wrote a little code in setup() to sample touchRead() twenty times
Code:
    delay(10000);
    touchR_threshhold = touchL_threshhold = touchSpiral_threshhold = touchBlink_threshhold = 0;
    
    //You've got more than 10 seconds after plugging battery in to get your hands away
    for( int i = 0; i <= 20; i++) {    //look 20 times; max value is the threshhold
      int a;
      if (a=touchRead(16) > touchR_threshhold) { touchR_threshhold = a;}
      if (a=touchRead(19) > touchL_threshhold) { touchL_threshhold = a;}
      a=touchRead(23);
      if (a > touchSpiral_threshhold) { touchSpiral_threshhold = a;}
      Serial.println(a);
      if (a=touchRead(22) > touchBlink_threshhold) { touchBlink_threshhold = a;}
    }
    touchR_threshhold += 2;
    touchL_threshhold += 2;
    touchSpiral_threshhold += 2;
    touchBlink_threshhold += 2;
    Serial.print(" touchSpiral_threshhold = ");
    Serial.println( touchSpiral_threshhold );
No hat or head yet, my knee 4-5 inches away, not moving. When I sample the same pins later in loop() the readings are considerably higher, about 860 for the one I called spiral and 777 for the one I called right. Now I can do something with millis() and get my baseline in loop() and probably will, later today. Maybe that's the best solution anyway, since 10 seconds after I attach the battery to put the hat with the hardware on my head before taking the baseline might not have been enough. But I'm really puzzled that with nothing I could see affecting the capacitance 'environment' the readings changed so much. I'm hoping someone can give me some insight.
 
I didn't try your code....but briefly looked through it ....and ...????

Looks like you have 4 conditions, lookRight, LookLeft, Spiral and Blink.
I assume each controlled by a separate touch pins... example pins 16, 17, 18 and 19 on Teensy3.2.
my approach would be....
mount teensy and display in the hat and run insulated wire from each pin to a different location on the hat....solder on a small metal pad on end to make it easy touched.

Set up a basic sketch to print out the reading from each pin when touched and also untouched.
serial.print(touchread(pin) for each pin

From this get an average middle (threshold) reading that when pin is > threshold do something for that pin and when pin is < threshold do something else for that pin or do nothing and respond to next pin touched ... then similar for rest on pins.

if touchread(pin) > threshold
{
do something
}
 
The baseline max reading I got for spiral was 800.


So the reading on one pin changed by about 60 without any proximity or touching change and the other pin reads about 85 units different from the one I looked at most closely.

I'm uneasy with the idea that switching to a battery rather than USB power and putting it on my head (instead of a table with my laptop but no other electronics) will give me the same readings, especially since my intuition about getting the same readings in setup() and loop() is so far off.

What strikes me, thinking a little longer, is that the ST7735 display has been initialized but isn't really getting output yet in setup. If I were thinking about the RF environment, rather than the capacitance environment, I would think that was definitely the reason for the discrepancy. If that's the issue then the delay(10 seconds) I put into the code I posted is hugely detrimental!
 
You need to decide and explain what you are trying to do.
I thought you were trying to copy the hat idea with some mods. touching it in different places on the hat made the eye blink or roll or look right or left.
RF does not come into it, use insulated wires with pads on end....it will work hanging on a clothes line never mind you head
 
Ick

ThisSmacksOfInterrupt.jpg

Unfortunately the program is BIG. The way that output got interrupted in the midst of a print statement and then trashed a global variable makes me think interrupt is involved, although I suppose I might be out of memory. Some excerpts:

Code:
Sketch uses 185496 bytes (70%) of program storage space. Maximum is 262144 bytes.
Global variables use 4704 bytes (7%) of dynamic memory, leaving 60832 bytes for local variables. Maximum is 65536 bytes.

before setup() I initialize the threshhold values to a large number which I replace with the first reading :
Code:
#if (defined(__MK20DX128__) || defined(__MK20DX256__) )  //teensy touch calibration.
  uint16_t touchR_threshhold = 16000;     //start with a big number
  uint16_t touchL_threshhold = 16000;
  uint16_t touchSpiral_threshhold = 16000;
  uint16_t touchBlink_threshhold = 16000;     //start with a big number
#elif
  uint16_t touchR_threshhold, touchL_threshhold, touchSpiral_threshhold, touchBlink_threshhold;
#endif

Then I look for the maximum value read in the first minute after power-up:
Code:
#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
#ifdef TOUCH_SPIRAL              //capacitative touch --> display cartoon hypnosis
    Serial.print("touch spiral = "); 
    Serial.print(touchRead(TOUCH_PIN_SPIRAL)); 
    if (millis()-startTime < 60000) {
        if (touchSpiral_threshhold == 16000) { touchSpiral_threshhold = touchRead(TOUCH_PIN_SPIRAL);}
        else  {
            if(int a = touchRead(TOUCH_PIN_SPIRAL) > touchSpiral_threshhold) {touchSpiral_threshhold = a+2;}
              } 
         }
    else {
        if  TOUCH_SPIRAL {
          eye[0].display->fillScreen(ST77XX_BLACK);   //another effect could be to skip this, draw cicrles over eye.
          int color = ST77XX_ORANGE;
    
          while TOUCH_SPIRAL {
            color = drawCircles( color );
            Serial.print("thresh= "); Serial.print(touchSpiral_threshhold); Serial.print("  touch="); Serial.println(TOUCH_PIN_SPIRAL);
            }
          }
        }
    }
#endif

I print out the values in a subroutine that runs several times a second:
Code:
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 = micros(); // Time at start of function

//debugging/exploratory jrr
Serial.print("Spiral=");Serial.print(touchSpiral_threshhold);
Serial.print("   Blink=");Serial.print(touchBlink_threshhold);
Serial.print("  L=");Serial.print(touchL_threshhold);
Serial.print("  R=");Serial.println(touchR_threshhold);
  if(!(++frames & 255)) { // Every 256 frames...
    uint32_t elapsed = (millis() - startTime) / 1000;


I'm happy to post all of the code, but its so big that I doubt anyone would dig through it. I'm not sure how to proceed to sort out precisely what happened. The memoryUsage library didn't seem to work in this context. The values for the capacitance pins vary so much from pin to pin (as you can see in the screenshot) that I still think something like this is desirable at some stage, but I'm thinking that hard coding the threshholds empirically is going to be better than figuring them out during the first minute after power-up.
 
I first saw touchread the other week - when I did my testing I started LOW - looking for minimum resting value when untouched - then added some buffer value to ignore near touches etc.

When untouched it will be some hundreds depending on device and wiring 400 to 800? Then on touch it exceeds some value regularly - but while touched the value will climb ever higher. I was seeing it work reliably with 5 resistors wired to copper tape. But touching the wires - cheapo premade pinned wires - that would trigger them - I didn't measure for that - they just need to be isolated. I didn't test not on USB but power and proximity to a head and such would seem to change value maybe depending on the setup.

I started a sketch - one posting is here
 
Last edited:
I created an adaptation of the touch library for binary touch (touched or not)
At startup (touchablePin object instantiation) I sample each pin, recording the untouched value for each pin.
I then call each pin touched when it is some multiple of the untouched value- typically around 1.5 times the untouched value.
While you need not do this in your case (if it’s ok to wait 100+ms for each touch) I mucked with the code at the heart of touch... a while statement, placing a check to see if the (untouched * 1.5) was reached, and bailing with tougpcged = true.

Does that help?
I could share my touchablePin library, which extends touch.c
 
I created an adaptation of the touch library for binary touch (touched or not)
At startup (touchablePin object instantiation) I sample each pin, recording the untouched value for each pin.
I then call each pin touched when it is some multiple of the untouched value- typically around 1.5 times the untouched value.
While you need not do this in your case (if it’s ok to wait 100+ms for each touch) I mucked with the code at the heart of touch... a while statement, placing a check to see if the (untouched * 1.5) was reached, and bailing with tougpcged = true.

Does that help?
I could share my touchablePin library, which extends touch.c

That library sounds like just what I was trying for! Do you physically touch it or just come close (i.e. will it jump by 1.5x on the other side of a layer of hat felt?)

I'm worried that this bug that trashes global variables (it actually gets all 4 of the threshhold values I was trying to find one at a time) will mess that up but it seems like scrapping the direction I've been going in (since I don't really see a way forward) is not a bad choice.

I am going to go ahead and post all of the code in case someone wants to go bug hunting. First the little config.h file:
Code:
// Pin selections here are based on the original Adafruit Learning System
// guide for the Teensy 3.x project.  Some of these pin numbers don't even
// exist on the smaller SAMD M0 & M4 boards, so you may need to make other
// selections:
//#define ADAFRUIT_HALLOWING
// 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.
#ifdef ADAFRUIT_HALLOWING // Hallowing, with one eye, does this by default
  #define SYMMETRICAL_EYELID
#else                     // Otherwise your choice, standard is asymmetrical
  //#define SYMMETRICAL_EYELID
#endif

// Enable ONE of these #includes -- HUGE graphics tables for various eyes:
#include "graphics/defaultEye.h"    // Standard human-ish hazel eye -OR-
//#include "graphics/dragonEye.h"   // Slit pupil fiery dragon/demon eye -OR-
//#include "graphics/noScleraEye.h" // Large iris, no sclera -OR-
//#include "graphics/goatEye.h"     // Horizontal pupil goat/Krampus eye -OR-
//#include "graphics/newtEye.h"     // Eye of newt

// Optional: enable this line for startup logo (screen test/orient):
#if !defined ADAFRUIT_HALLOWING     // Hallowing can't always fit logo+eye
 // #include "graphics/logo.h"        // Otherwise your choice, if it fits
#endif

// EYE LIST ----------------------------------------------------------------

// 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), and a screen
// rotation value (0-3) for that eye.

eyeInfo_t eyeInfo[] = {
#ifdef ADAFRUIT_HALLOWING
  { 39, -1, 2 }, // SINGLE EYE display-select and wink pins, rotate 180
#else
  {  9, 0, 0 }, // LEFT EYE display-select and wink pins, no rotation
 // { 10, 2, 0 }, // RIGHT EYE display-select and wink pins, no rotation
#endif
};

// DISPLAY HARDWARE SETTINGS (screen type & connections) -------------------

#ifdef ADAFRUIT_HALLOWING
  #include <Adafruit_ST7735.h> // TFT display library
  #include <Adafruit_FreeTouch.h>
  #define DISPLAY_DC       38  // Display data/command pin
  #define DISPLAY_RESET    37  // Display reset pin
  #define DISPLAY_BACKLIGHT 7
  #define BACKLIGHT_MAX   128
#else
  // Enable ONE of these #includes to specify the display type being used
  //#include <Adafruit_SSD1351.h>  // OLED display library -OR-
  //#include <Adafruit_ST7735.h> // TFT display library (enable one only)
  #include <Adafruit_ST7789.h>
  #define DISPLAY_DC        7    // Data/command pin for ALL displays
  #define DISPLAY_RESET     8    // Reset pin for ALL displays
#endif

#if defined(_ADAFRUIT_ST7735H_) || defined(_ADAFRUIT_ST77XXH_)
  #define SPI_FREQ 24000000    // TFT: use max SPI (clips to 12 MHz on M0)
#else // OLED
  #if !defined(ARDUINO_ARCH_SAMD) && (F_CPU <= 72000000)
    #define SPI_FREQ 24000000  // OLED: 24 MHz on 72 MHz Teensy only
  #else
    #define SPI_FREQ 12000000  // OLED: 12 MHz in all other cases
  #endif
#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 (")
#if defined ADAFRUIT_HALLOWING
    #define TOUCH_RIGHT  (qt_1.measure() > touchR_threshhold)       //use Hallowing touch pins Joystick full right
    #define TOUCH_LEFT  (qt_4.measure() > touchL_threshhold)        //Joystick full left
    #define TOUCH_SPIRAL  (qt_2.measure() > 700)      //simulate cartoon hypnosis spiral
    #define TOUCH_BLINK  (qt_3.measure() > 700)       //like blink pin below
#elif (defined(__MK20DX128__) || defined(__MK20DX256__) )
    #define TOUCH_PIN_RIGHT 16
    #define TOUCH_PIN_LEFT 19
    #define TOUCH_PIN_SPIRAL 23
    #define TOUCH_PIN_BLINK 22
    #define TOUCH_RIGHT  (touchRead(TOUCH_PIN_RIGHT)>touchR_threshhold)       //use Teensy touch pins Joystick full right
    #define TOUCH_LEFT  (touchRead(TOUCH_PIN_LEFT)>touchL_threshhold)        //Joystick full left
    #define TOUCH_SPIRAL  (touchRead(TOUCH_PIN_SPIRAL)>touchSpiral_threshhold)      //simulate cartoon hypnosis spiral
    #define TOUCH_BLINK  (touchRead(TOUCH_PIN_BLINK)>touchBlink_threshhold)       //like blink pin below
#endif
//#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 BLINK_PIN         1 // Pin for manual blink button (BOTH eyes)
#define AUTOBLINK           // If defined, eyes also blink autonomously
#ifdef ADAFRUIT_HALLOWING
  #define LIGHT_PIN      A1 // Hallowing light sensor pin
  #define LIGHT_CURVE  0.33 // Light sensor adjustment curve
  #define LIGHT_MIN      30 // Minimum useful reading from light sensor
  #define LIGHT_MAX     980 // Maximum useful reading from sensor
#else
  #define LIGHT_PIN      A2 // Photocell or potentiometer (else auto iris)
//#define LIGHT_PIN_FLIP    // If defined, reverse reading from dial/photocell
  #define LIGHT_MIN       0 // Lower reading from sensor
  #define LIGHT_MAX    1023 // Upper reading from sensor
#endif
#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      120 // Iris size (0-1023) in brightest light
#endif
#if !defined(IRIS_MAX)
  #define IRIS_MAX      720 // Iris size (0-1023) in darkest light
#endif

and the main program
Code:
//--------------------------------------------------------------------------
// 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.
//--------------------------------------------------------------------------

#include <SPI.h>
#include <Adafruit_GFX.h>
#ifdef ARDUINO_ARCH_SAMD
  #include <Adafruit_ZeroDMA.h>
#endif
include <MemoryUsage.h>

typedef struct {        // Struct is defined before including config.h --
  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.
} eyeInfo_t;

#include "config.h"     // ****** CONFIGURATION IS DONE IN HERE ******

#if defined(_ADAFRUIT_ST7735H_) 
  typedef Adafruit_ST7735  displayType; // Using TFT display(s)
#elif defined(_ADAFRUIT_ST7789H_)
  typedef Adafruit_ST7789  displayType;
#elif defined(_ADAFRUIT_SSD1351_)
  typedef Adafruit_SSD1351 displayType; // Using OLED display(s)
#endif

#if !( defined(_ADAFRUIT_ST7735H_) || defined(_ADAFRUIT_ST7789H_) ||  defined (_ADAFRUIT_SSD1351_) )
    #error "No display defined in config.h"
#endif

// 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;

#define NUM_EYES (sizeof eyeInfo / sizeof eyeInfo[0]) // config.h pin list

struct {                // One-per-eye structure
  displayType *display; // -> OLED/TFT object
  eyeBlink     blink;   // Current blink/wink state
} eye[NUM_EYES];

#ifdef ARDUINO_ARCH_SAMD
  // SAMD boards use DMA (Teensy uses SPI FIFO instead):
  // Two single-line 128-pixel buffers (16bpp) are used for DMA.
  // Though you'd think fewer larger transfers would improve speed,
  // multi-line buffering made no appreciable difference.
  uint16_t          dmaBuf[2][128];
  uint8_t           dmaIdx = 0; // Active DMA buffer # (alternate fill/send)
  Adafruit_ZeroDMA  dma;
  DmacDescriptor   *descriptor;

  // DMA transfer-in-progress indicator and callback
  static volatile bool dma_busy = false;
  static void dma_callback(Adafruit_ZeroDMA *dma) { dma_busy = false; }
#endif


uint32_t startTime;  // For FPS indicator
#if (defined(__MK20DX128__) || defined(__MK20DX256__) )  //teensy touch calibration.
  uint16_t touchR_threshhold = 16000;     //start with a big number
  uint16_t touchL_threshhold = 16000;
  uint16_t touchSpiral_threshhold = 16000;
  uint16_t touchBlink_threshhold = 16000;     //start with a big number
#elif
  uint16_t touchR_threshhold, touchL_threshhold, touchSpiral_threshhold, touchBlink_threshhold;
#endif
#ifdef ADAFRUIT_HALLOWING
  Adafruit_FreeTouch qt_1 = Adafruit_FreeTouch(A2, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE);    // Joystick full right
  Adafruit_FreeTouch qt_2 = Adafruit_FreeTouch(A3, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE);    //Joystick full left
  Adafruit_FreeTouch qt_3 = Adafruit_FreeTouch(A4, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE);    //simulate cartoon hypnosis spiral
  Adafruit_FreeTouch qt_4 = Adafruit_FreeTouch(A5, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE);    //like blink pin 
  touchR_threshhold = touchL_threshhold = touchSpiral_threshhold = touchBlink_threshhold = 700;
#endif



// INITIALIZATION -- runs once at startup ----------------------------------

void setup(void) {
  uint8_t e; // Eye index, 0 to NUM_EYES-1

  Serial.begin(115200);
  randomSeed(analogRead(A3)); // Seed random() from floating analog input

  
  #ifdef ADAFRUIT_HALLOWING
    Serial.println("Hallowing Defined");
    if (! qt_1.begin())  
      Serial.println("Failed to begin qt on pin A2");
    if (! qt_2.begin())  
      Serial.println("Failed to begin qt on pin A3");
    if (! qt_3.begin())  
      Serial.println("Failed to begin qt on pin A4");
    if (! qt_4.begin())  
      Serial.println("Failed to begin qt on pin A5");
  #endif
  
  

#ifdef DISPLAY_BACKLIGHT
  // Enable backlight pin, initially off
  pinMode(DISPLAY_BACKLIGHT, OUTPUT);
  digitalWrite(DISPLAY_BACKLIGHT, LOW);
#endif

  // Initialize eye objects based on eyeInfo list in config.h:
  for(e=0; e<NUM_EYES; e++) {
    eye[e].display     = new displayType(eyeInfo[e].select, DISPLAY_DC, -1);
    eye[e].blink.state = NOBLINK;
    // If project involves only ONE eye and NO other SPI devices, its
    // select line can be permanently tied to GND and corresponding pin
    // in config.h set to -1.  Best to use it though.
    if(eyeInfo[e].select >= 0) {
      pinMode(eyeInfo[e].select, OUTPUT);
      digitalWrite(eyeInfo[e].select, HIGH); // Deselect them all
    }
    // 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

#if defined(DISPLAY_RESET) && (DISPLAY_RESET >= 0)
  // Because both displays share a common reset pin, -1 is passed to
  // the display constructor above to prevent the begin() function from
  // resetting both displays after one is initialized.  Instead, handle
  // the reset manually here to take care of both displays just once:
  pinMode(DISPLAY_RESET, OUTPUT);
  digitalWrite(DISPLAY_RESET, LOW);  delay(1);
  digitalWrite(DISPLAY_RESET, HIGH); delay(50);
  // Alternately, all display reset pin(s) could be connected to the
  // microcontroller reset, in which case DISPLAY_RESET should be set
  // to -1 or left undefined in config.h.
#endif

  // After all-displays reset, now call init/begin func for each display:
  for(e=0; e<NUM_EYES; e++) {
#if defined(_ADAFRUIT_ST7735H_)  // TFT
    eye[e].display->initR(INITR_144GREENTAB)
#elif defined(_ADAFRUIT_ST7789H_)
    eye[e].display->init(240,240);         // jrr 
#else // OLED
    eye[e].display->begin();
#endif
    eye[e].display->setRotation(eyeInfo[e].rotation);
  }

#if defined(LOGO_TOP_WIDTH) || defined(COLOR_LOGO_WIDTH)
  // I noticed lots of folks getting right/left eyes flipped, or
  // installing upside-down, etc.  Logo split across screens may help:
  for(e=0; e<NUM_EYES; e++) { // Another pass, after all screen inits
    eye[e].display->fillScreen(0);
    #ifdef LOGO_TOP_WIDTH
      // Monochrome Adafruit logo is 2 mono bitmaps:
      eye[e].display->drawBitmap(NUM_EYES*64 - e*128 - 20,
        0, logo_top, LOGO_TOP_WIDTH, LOGO_TOP_HEIGHT, 0xFFFF);
      eye[e].display->drawBitmap(NUM_EYES*64 - e*128 - LOGO_BOTTOM_WIDTH/2,
        LOGO_TOP_HEIGHT, logo_bottom, LOGO_BOTTOM_WIDTH, LOGO_BOTTOM_HEIGHT,
        0xFFFF);
    #else
      // Color sponsor logo is one RGB bitmap:
      eye[e].display->fillScreen(color_logo[0]);
      eye[0].display->drawRGBBitmap(
        (eye[e].display->width()  - COLOR_LOGO_WIDTH ) / 2,
        (eye[e].display->height() - COLOR_LOGO_HEIGHT) / 2,
        color_logo, COLOR_LOGO_WIDTH, COLOR_LOGO_HEIGHT);
    #endif
    // After logo is drawn
  }
  #ifdef DISPLAY_BACKLIGHT
    int i;
    for(i=0; i<BACKLIGHT_MAX; i++) { // Fade logo in
      analogWrite(DISPLAY_BACKLIGHT, i);
      delay(2);
    }
    delay(1400); // Pause for screen layout/orientation
    for(; i>=0; i--) {
      analogWrite(DISPLAY_BACKLIGHT, i);
      delay(2);
    }
    for(e=0; e<NUM_EYES; e++) { // Clear display(s)
      eye[e].display->fillScreen(0);
    }
    delay(100);
  #else
    delay(2000); // Pause for screen layout/orientation
  #endif // DISPLAY_BACKLIGHT
#endif // LOGO_TOP_WIDTH

  // 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!
#if defined(_ADAFRUIT_ST7735H_) || defined(_ADAFRUIT_ST77XXH_) // TFT
  const uint8_t mirrorTFT[]  = { 0x88, 0x28, 0x48, 0xE8 }; // Mirror+rotate
  digitalWrite(eyeInfo[0].select, LOW);
  digitalWrite(DISPLAY_DC, LOW);
  #ifdef ST77XX_MADCTL
    SPI.transfer(ST77XX_MADCTL); // Current TFT lib
  #else
    SPI.transfer(ST7735_MADCTL); // Older TFT lib
  #endif
  digitalWrite(DISPLAY_DC, HIGH);
  SPI.transfer(mirrorTFT[eyeInfo[0].rotation & 3]);
  digitalWrite(eyeInfo[0].select , HIGH);
#else // OLED
  const uint8_t rotateOLED[] = { 0x74, 0x77, 0x66, 0x65 },
                mirrorOLED[] = { 0x76, 0x67, 0x64, 0x75 }; // Mirror+rotate
  // If OLED, loop through ALL eyes and set up remap register
  // from either mirrorOLED[] (first eye) or rotateOLED[] (others).
  // The OLED library doesn't normally use the remap reg (TFT does).
  for(e=0; e<NUM_EYES; e++) {
    eye[e].display->writeCommand(SSD1351_CMD_SETREMAP);
    eye[e].display->writeData(e ?
      rotateOLED[eyeInfo[e].rotation & 3] :
      mirrorOLED[eyeInfo[e].rotation & 3]);
  }
#endif

#ifdef ARDUINO_ARCH_SAMD
  // Set up SPI DMA on SAMD boards:
  int                dmac_id;
  volatile uint32_t *data_reg;
  if(&PERIPH_SPI == &sercom0) {
    dmac_id  = SERCOM0_DMAC_ID_TX;
    data_reg = &SERCOM0->SPI.DATA.reg;
#if defined SERCOM1
  } else if(&PERIPH_SPI == &sercom1) {
    dmac_id  = SERCOM1_DMAC_ID_TX;
    data_reg = &SERCOM1->SPI.DATA.reg;
#endif
#if defined SERCOM2
  } else if(&PERIPH_SPI == &sercom2) {
    dmac_id  = SERCOM2_DMAC_ID_TX;
    data_reg = &SERCOM2->SPI.DATA.reg;
#endif
#if defined SERCOM3
  } else if(&PERIPH_SPI == &sercom3) {
    dmac_id  = SERCOM3_DMAC_ID_TX;
    data_reg = &SERCOM3->SPI.DATA.reg;
#endif
#if defined SERCOM4
  } else if(&PERIPH_SPI == &sercom4) {
    dmac_id  = SERCOM4_DMAC_ID_TX;
    data_reg = &SERCOM4->SPI.DATA.reg;
#endif
#if defined SERCOM5
  } else if(&PERIPH_SPI == &sercom5) {
    dmac_id  = SERCOM5_DMAC_ID_TX;
    data_reg = &SERCOM5->SPI.DATA.reg;
#endif
  }

  dma.allocate();
  dma.setTrigger(dmac_id);
  dma.setAction(DMA_TRIGGER_ACTON_BEAT);
  descriptor = dma.addDescriptor(
    NULL,               // move data
    (void *)data_reg,   // to here
    sizeof dmaBuf[0],   // this many...
    DMA_BEAT_SIZE_BYTE, // bytes/hword/words
    true,               // increment source addr?
    false);             // increment dest addr?
  dma.setCallback(dma_callback);

#endif // End SAMD-specific SPI DMA init

#ifdef DISPLAY_BACKLIGHT
  analogWrite(DISPLAY_BACKLIGHT, BACKLIGHT_MAX);
#endif
   

  startTime = millis(); // For frame-rate calculation
}


// EYE-RENDERING FUNCTION --------------------------------------------------

SPISettings settings(SPI_FREQ, MSBFIRST, SPI_MODE0);

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;
  uint32_t d;

  // 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.
  SPI.beginTransaction(settings);
  digitalWrite(eyeInfo[e].select, LOW);                        // Chip select
#if defined(_ADAFRUIT_ST7735H_) || defined(_ADAFRUIT_ST77XXH_) // TFT
  eye[e].display->setAddrWindow(0, 0, 128, 128);
#else // OLED
  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
#endif
  digitalWrite(eyeInfo[e].select, LOW);                // Re-chip-select
  digitalWrite(DISPLAY_DC, HIGH);                      // Data mode
  // 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;
  for(screenY=0; screenY<SCREEN_HEIGHT; screenY++, scleraY++, irisY++) {
#ifdef ARDUINO_ARCH_SAMD
    uint16_t *ptr = &dmaBuf[dmaIdx][0];
#endif
    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
        }
      }
#ifdef ARDUINO_ARCH_SAMD
      *ptr++ = __builtin_bswap16(p); // DMA: store in scanline buffer
#else
      // SPI FIFO technique from Paul Stoffregen's ILI9341_t3 library:
      while(KINETISK_SPI0.SR & 0xC000); // Wait for space in FIFO
      KINETISK_SPI0.PUSHR = p | SPI_PUSHR_CTAS(1) | SPI_PUSHR_CONT;
#endif
    } // end column
#ifdef ARDUINO_ARCH_SAMD
    while(dma_busy); // Wait for prior DMA xfer to finish
    descriptor->SRCADDR.reg = (uint32_t)&dmaBuf[dmaIdx] + sizeof dmaBuf[0];
    dma_busy = true;
    dmaIdx   = 1 - dmaIdx;
    dma.startJob();
#endif
  } // end scanline

#ifdef ARDUINO_ARCH_SAMD
  while(dma_busy);  // Wait for last scanline to transmit
#else
  KINETISK_SPI0.SR |= SPI_SR_TCF;         // Clear transfer flag
  while((KINETISK_SPI0.SR & 0xF000) ||    // Wait for SPI FIFO to drain
       !(KINETISK_SPI0.SR & SPI_SR_TCF)); // Wait for last bit out
#endif

  digitalWrite(eyeInfo[e].select, HIGH);          // Deselect
  SPI.endTransaction();
}


// 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

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 = micros(); // Time at start of function

//debugging/exploratory jrr
Serial.print("Spiral=");Serial.print(touchSpiral_threshhold);
Serial.print("   Blink=");Serial.print(touchBlink_threshhold);
Serial.print("  L=");Serial.print(touchL_threshhold);
Serial.print("  R=");Serial.println(touchR_threshhold);

  if(!(++frames & 255)) { // Every 256 frames...
    uint32_t elapsed = (millis() - startTime) / 1000;
    if(elapsed) Serial.println(frames / elapsed); // Print FPS
  }

  if(++eyeIndex >= NUM_EYES) eyeIndex = 0; // Cycle through eyes, 1 per call
#if (defined(__MK20DX128__) || defined(__MK20DX256__) )  //teensy touch calibration.
  uint16_t touchR_threshhold = 16000;     //start with a big number
  uint16_t touchL_threshhold = 16000;
  uint16_t touchSpiral_threshhold = 16000;
  uint16_t touchBlink_threshhold = 16000;     //start with a big number
#elif
  uint16_t touchR_threshhold, touchL_threshhold, touchSpiral_threshhold, touchBlink_threshhold;
#endif
  // 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 boolean  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);
        #if defined (TOUCH_LEFT)
        
            #ifdef TOUCH_PIN_RIGHT
              if (millis()-startTime < 60*1000) {
                if (touchR_threshhold == 16000) {touchR_threshhold = touchRead(TOUCH_PIN_RIGHT);}
              else {
                if (int a = touchRead(TOUCH_PIN_RIGHT) > touchR_threshhold) {touchR_threshhold = a +2;}
                   }
              }
              if (millis()-startTime < 60*1000) {
                if (touchL_threshhold == 16000)  {touchL_threshhold = touchRead(TOUCH_PIN_LEFT);}
                else {
                  if (int a = touchRead(TOUCH_PIN_LEFT) > touchL_threshhold) {touchL_threshhold = a+2;}
                }
              }
            #endif

           if TOUCH_LEFT  {
              eyeNewX = 1;
              Serial.println("LEFT pin detected.");
              }
           if TOUCH_RIGHT {
              eyeNewX = 999;
              Serial.println("RIGHT pin detected.");
              }
        #endif
        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 defined(TOUCH_BLINK)  
  if (millis()-startTime < 60*1000) {
        if (millis()-startTime < 60*1000) {
          if (touchBlink_threshhold == 16000) {touchBlink_threshhold = touchRead(TOUCH_PIN_BLINK);}
        else {
          if (int a = touchRead(TOUCH_PIN_BLINK) > touchR_threshhold) {touchBlink_threshhold = a+2;}
             }
        }
    }
#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
#if defined(TOUCH_BLINK)
        TOUCH_BLINK ||
#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)) || defined(TOUCH_BLINK)
  #if defined(BLINK_PIN) && (BLINK_PIN >= 0)
    if(digitalRead(BLINK_PIN) == LOW) {
  #endif
  #if defined(TOUCH_BLINK)
     if TOUCH_BLINK {
       Serial.println("Blink pin detected.");
  #endif
     }
      // 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      jrr
#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

  // 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...
  if(NUM_EYES > 1) 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() {

#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
#ifdef TOUCH_SPIRAL              //capacitative touch --> display cartoon hypnosis
    Serial.print("touch spiral = "); 
    Serial.print(touchRead(TOUCH_PIN_SPIRAL)); 
    if (millis()-startTime < 60000) {
        if (touchSpiral_threshhold == 16000) { touchSpiral_threshhold = touchRead(TOUCH_PIN_SPIRAL);}
        else  {
            if(int a = touchRead(TOUCH_PIN_SPIRAL) > touchSpiral_threshhold) {touchSpiral_threshhold = a+2;}
              } 
         }
    else {
        if  TOUCH_SPIRAL {
          eye[0].display->fillScreen(ST77XX_BLACK);   //another effect could be to skip this, draw cicrles over eye.
          int color = ST77XX_ORANGE;
    
          while TOUCH_SPIRAL {
            color = drawCircles( color );
            Serial.print("thresh= "); Serial.print(touchSpiral_threshhold); Serial.print("  touch="); Serial.println(TOUCH_PIN_SPIRAL);
            }
          }
        }
    }
#endif


int drawCircles( uint16_t color) {
  int  centerx = eye[0].display->width()/2;
  int centery = eye[0].display->height()/2;
  for (int radius=eye[0].display->height()/2; radius > 5; radius -=10) {
    eye[0].display->drawCircle(centerx,centery, radius, color);
    eye[0].display->drawCircle(centerx,centery, radius+1, color);
    eye[0].display->drawCircle(centerx,centery, radius+2, color);
    eye[0].display->drawCircle(centerx,centery, radius+3, color);
    color +=2421;
  }
  return color;
}
 
That is a big chunk of code! Sounds like fun.
Here's the touchablePin library I wrote based on Paul S's touchPin library in the standard Teensy code.
Some documentation in the readme

Place the contents in arduino/libraries/touchablePin

View attachment touchablePin.zip

Good luck!

Dave
 
Thanks I will give it a try!

I used touchablePin and it is back to working for a while. I just implemented the spiral effect so far. I do have to touch the pad directly, it doesn't work through the hat felt. The program doesn't seem stable beyond 5 or 10 minutes though, it starts alternating between the spiral and the eyeball without me touching. I wonder if I should call pin.initUntouched(); every minute or so?

The readme file was very helpful, I had not thought the pins charged up over time so that helped enormously by pointing me in a direction I had not known to look.
 
Two things to try:

1 - lower MAX_FACTOR. The default 1.3 factor is typical for sensing finger-to-metal light touch.

The touch routines do sense proximity.

Play with the touchablePin.read() method to see what the values show at rest (hands away from hat) and as your hand approaches a touch of the felt.

I expect it will be a smaller, but consistent ratio.

2 - bigger flat metal buttons behind the felt. See it you can devise a flat circle as the capacitor under the felt (1/2”?)

I don’t know that this will work, but my instinct is that the capacitance of a small metal plate will be more affected than a similarly-connected simple wire
 
Two things to try:

1 - lower MAX_FACTOR. The default 1.3 factor is typical for sensing finger-to-metal light touch.

The touch routines do sense proximity.

Play with the touchablePin.read() method to see what the values show at rest (hands away from hat) and as your hand approaches a touch of the felt.

I expect it will be a smaller, but consistent ratio.

2 - bigger flat metal buttons behind the felt. See it you can devise a flat circle as the capacitor under the felt (1/2”?)

I don’t know that this will work, but my instinct is that the capacitance of a small metal plate will be more affected than a similarly-connected simple wire
I had 1" foil so the pad is about 1' square. 1.15 didn't work through the hat but 1.1 did.

I had that spurious activity last night after it ran 5-10 minutes. I left it running (with the default max) overnight--maybe 20 hrs-- and it seemed stable every time I looked.

I'll move forward with battery etc. Thanks again!!
 
I had 1" foil so the pad is about 1' square. 1.15 didn't work through the hat but 1.1 did.

I had that spurious activity last night after it ran 5-10 minutes. I left it running (with the default max) overnight--maybe 20 hrs-- and it seemed stable every time I looked.

I'll move forward with battery etc. Thanks again!!

It seems to be working better after I increased the number of samples. If time to charge the capacitor is a factor that makes a lot of sense: it does several samples in rapid succession and gets a more consistent result than samples taken at variable intervals. I do want it to be stable for six hours or more.
 
Why do you want it working through the felt on the hat..??...what is wrong with a few metal buttons or studs.....it will work with that for the most basic of sketches using the existing library........what will happen in your case if the hat gets wet ....wet rain is forecast for Halloween night ...???....
 
Just a thought... use small screws (bolts) and nuts for my touch prototypes (see pics - for reference, the screw heads are 6.6mm wide)
If you had a conductive paint, the heads could be painted black.
Washers are, of course, optional.
The nut on the inside allows attaching a wire easily.
I put a spade connector on the wire for easy reuse
IMG_2274.jpgIMG_2275.jpg
 
Too much like an engineering solution, Go to the cobblers that mends shoes and fixes leather goods, get some metal eyelets like used for laced shoes, put a row around the rim, there is a tool for crimping them on and that will hold the wire too, then get some boot laces and thread them through and leave the laces dangling down like hair.....wood be good for Halloween see studded hat attachedStuddedHat2.JPG
 
Touching a piece of metal directly connected to a touch sensing pin works fine, until you discharge a bolt of static electricity into the pin, which is why capacitive touch sensors aren't made that way.

-- rec --
 
That is a big chunk of code! Sounds like fun.
Here's the touchablePin library I wrote based on Paul S's touchPin library in the standard Teensy code.
Some documentation in the readme

Place the contents in arduino/libraries/touchablePin

View attachment 14915

Good luck!

Dave
Dave

The concept of the touchablePin library is nice.
Also the fact that you set it to touched if half of a nr of samples are high. Nice noise reduction. Although I would to that on next "update" readings from the program.
 
Back
Top