diff --git a/Qualia_ESP32S3_Eyes/BmpClass.h b/Qualia_ESP32S3_Eyes/BmpClass.h new file mode 100644 index 000000000..d55a0f155 --- /dev/null +++ b/Qualia_ESP32S3_Eyes/BmpClass.h @@ -0,0 +1,251 @@ +/******************************************************************************* + * BMP Class + * + * Rewrite from: https://github.com/Jaycar-Electronics/Arduino-Picture-Frame.git + ******************************************************************************/ +#ifndef _BMPCLASS_H_ +#define _BMPCLASS_H_ + +#include "globals.h" +typedef void(BMP_DRAW_CALLBACK)(int16_t x, int16_t y, uint16_t *bitmap, int16_t w, int16_t h); + +class BmpClass +{ +public: + void draw( + File *f, BMP_DRAW_CALLBACK *bmpDrawCallback, bool useBigEndian, + int16_t x, int16_t y, int16_t widthLimit, int16_t heightLimit) + { + _bmpDrawCallback = bmpDrawCallback; + _useBigEndian = useBigEndian; + _heightLimit = heightLimit; + + int16_t u, v; + uint32_t xend; + + getbmpparms(f); + + //validate bitmap + if ((bmtype == 19778) && (bmwidth > 0) && (bmheight > 0) && (bmbpp > 0)) + { + //centre image + u = (widthLimit - bmwidth) / 2; + v = (heightLimit - bmheight) / 2; + u = (u < 0) ? x : x + u; + v = (v < 0) ? y : y + v; + xend = (bmwidth > widthLimit) ? widthLimit : bmwidth; + + bmpRow = (uint16_t *)malloc(xend * 2); + if (!bmpRow) + { + Serial.println(F("bmpRow malloc failed.")); + } + if (bmbpp < 9) + { + bmplt = (uint16_t *)malloc(bmpltsize * 2); + if (!bmplt) + { + Serial.println(F("bmplt malloc failed.")); + } + bmloadplt(f); //load palette if palettized + drawbmpal(f, u, v, xend); + free(bmplt); + } + else if (bmbpp == 16) + { + // TODO: bpp 16 should have 3 pixel types + drawbmRgb565(f, u, v, xend); + } + else + { + drawbmtrue(f, u, v, xend); + } + free(bmpRow); + } + } + +private: + void bmloadplt(File *f) + { + byte r, g, b; + if (bmpltsize == 0) + { + bmpltsize = 1 << bmbpp; //load default palette size + } + f->seek(54); //palette position in type 0x28 bitmaps + for (int16_t i = 0; i < bmpltsize; i++) + { + b = f->read(); + g = f->read(); + r = f->read(); + f->read(); //dummy byte + bmplt[i] = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3); + } + } + + void drawbmpal(File *f, int16_t u, int16_t v, uint32_t xend) + { + byte bmbitmask; + int16_t i, ystart, bmppb, p, d; + int16_t x, y; + uint16_t c; + bmbpl = ((bmbpp * bmwidth + 31) / 32) * 4; //bytes per line + bmppb = 8 / bmbpp; //pixels/byte + bmbitmask = ((1 << bmbpp) - 1); //mask for each pixel + ystart = 0; + if (bmheight > _heightLimit) + { + ystart = bmheight - _heightLimit; //don't draw if it's outside screen + } + for (y = ystart; y < bmheight; y++) + { //invert in calculation (y=0 is bottom) + f->seek(bmdataptr + y * bmbpl); //seek to start of line + x = 0; + p = 0; + while (x < xend) + { + if (p < 1) + { + d = f->read(); + p = bmppb; + } + d = d << bmbpp; + c = bmplt[(bmbitmask & (d >> 8))]; + bmpRow[x] = (_useBigEndian) ? ((c >> 8) | (c << 8)) : c; + + p--; + x++; + } + _bmpDrawCallback(u, v + bmheight - 1 - y, bmpRow, xend, 1); + } + } + + // draw 16-bit colour (RGB565) bitmap + void drawbmRgb565(File *f, int16_t u, int16_t v, uint32_t xend) + { + int16_t i, ystart; + uint32_t x, y; + byte lo, hi; + bmbpl = ((bmbpp * bmwidth + 31) / 32) * 4; //bytes per line, due to 32bit chunks + ystart = 0; + if (bmheight > _heightLimit) + { + ystart = bmheight - _heightLimit; //don't draw if it's outside screen + } + Serial.println(xend); + for (y = ystart; y < bmheight; y++) + { //invert in calculation (y=0 is bottom) + f->seek(bmdataptr + (y * bmbpl)); //seek at start of line + for (x = 0; x < xend; x++) + { + lo = f->read(); + hi = f->read(); + if (_useBigEndian) + { + bmpRow[x] = hi | lo << 8; + } + else + { + bmpRow[x] = lo | hi << 8; + } + } + _bmpDrawCallback(u, v + bmheight - 1 - y, bmpRow, xend, 1); + } + } + + // draw true colour bitmap at (u,v) handles 24/32 not 16bpp yet + void drawbmtrue(File *f, int16_t u, int16_t v, uint32_t xend) + { + int16_t i, ystart; + uint32_t x, y; + byte r, g, b; + bmbpl = ((bmbpp * bmwidth + 31) / 32) * 4; //bytes per line, due to 32bit chunks + ystart = 0; + if (bmheight > _heightLimit) + { + ystart = bmheight - _heightLimit; //don't draw if it's outside screen + } + for (y = ystart; y < bmheight; y++) + { //invert in calculation (y=0 is bottom) + f->seek(bmdataptr + y * bmbpl); //seek at start of line + for (x = 0; x < xend; x++) + { + b = f->read(); + g = f->read(); + r = f->read(); + if (bmbpp == 32) + { + f->read(); //dummy byte for 32bit + } + bmpRow[x] = (_useBigEndian) ? ((r & 0xf8) | (g >> 5) | ((g & 0x1c) << 11) | ((b & 0xf8) << 5)) : (((r & 0xf8) << 8) | ((g & 0xfc) << 3) | (b >> 3)); + } + _bmpDrawCallback(u, v + bmheight - 1 - y, bmpRow, xend, 1); + } + } + + void getbmpparms(File *f) + { //load into globals as ints-some parameters are 32 bit, but we can't handle this size anyway + byte h[48]; //header is 54 bytes typically, but we don't need it all + int16_t i; + f->seek(0); //set start of file + for (i = 0; i < 48; i++) + { + h[i] = f->read(); //read header + } + bmtype = h[0] + (h[1] << 8); //offset 0 'BM' + bmdataptr = h[10] + (h[11] << 8); //offset 0xA pointer to image data + bmhdrsize = h[14] + (h[15] << 8); //dib header size (0x28 is usual) + //files may vary here, if !=28, unsupported type, put default values + bmwidth = 0; + bmheight = 0; + bmbpp = 0; + bmpltsize = 0; + if ((bmhdrsize == 0x28) || (bmhdrsize == 0x38)) + { + bmwidth = h[18] + (h[19] << 8); //width + bmheight = h[22] + (h[23] << 8); //height + bmbpp = h[28] + (h[29] << 8); //bits per pixel + bmpltsize = h[46] + (h[47] << 8); //palette size + } + // Serial.printf("bmtype: %d, bmhdrsize: %d, bmwidth: %d, bmheight: %d, bmbpp: %d\n", bmtype, bmhdrsize, bmwidth, bmheight, bmbpp); + } + + byte isbmp(char n[]) + { //check if bmp extension + int16_t k; + k = strlen(n); + if (k < 5) + { + return 0; //name not long enough + } + if (n[k - 1] != 'P') + { + return 0; + } + if (n[k - 2] != 'M') + { + return 0; + } + if (n[k - 3] != 'B') + { + return 0; + } + if (n[k - 4] != '.') + { + return 0; + } + return 1; //passes all tests + } + + BMP_DRAW_CALLBACK *_bmpDrawCallback; + bool _useBigEndian; + int16_t _heightLimit; + + uint16_t bmtype, bmdataptr; //from header + uint32_t bmhdrsize, bmwidth, bmheight, bmbpp, bmpltsize; //from DIB Header + uint16_t bmbpl; //bytes per line- derived + uint16_t *bmplt; //palette- stored encoded for LCD + uint16_t *bmpRow; +}; + +#endif // _BMPCLASS_H_ \ No newline at end of file diff --git a/Qualia_ESP32S3_Eyes/README.md b/Qualia_ESP32S3_Eyes/README.md new file mode 100644 index 000000000..f3633a43f --- /dev/null +++ b/Qualia_ESP32S3_Eyes/README.md @@ -0,0 +1,20 @@ +## Adafruit Learning System - M4_Eyes for the MONSTER M4SK + +Make blinking, moving eyes on an Adafruit Monster M4sk microcontroller and display board. + +This code is used for displaying realistic eyes on the Adafruit [Monster M4sk](https://www.adafruit.com/product/4343) board. + +See the main [Adafruit Learning System tutorial on the Monster M4sk here](https://learn.adafruit.com/adafruit-monster-m4sk-eyes/overview). + +Note the code is such that user code may be run with linitations - see user*.cpp. This method is used for a number of Adafruit Learning System guides to provide various functionality. As this is a rough form of task switching, not all actions will be compatible. There be dragons here and there is no guarantee of compatibility with all types of actions and hardware. + +Adafruit invests time and resources providing this open source code, +please support Adafruit and open-source hardware by purchasing +products from [Adafruit](https://www.adafruit.com)! + +MIT license, hardware by Limor "Ladyada" Fried, eye code by Phil Burgess + +All text above must be included in any redistribution + +----------------------- +If you are looking to make changes/additions, please use the GitHub Issues and Pull Request mechanisms. diff --git a/Qualia_ESP32S3_Eyes/file.cpp b/Qualia_ESP32S3_Eyes/file.cpp new file mode 100644 index 000000000..9016fdd47 --- /dev/null +++ b/Qualia_ESP32S3_Eyes/file.cpp @@ -0,0 +1,378 @@ +// SPDX-FileCopyrightText: 2019 Phillip Burgess for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +//34567890123456789012345678901234567890123456789012345678901234567890123456 + +#define ARDUINOJSON_ENABLE_COMMENTS 1 +#include // JSON config file functions + +#include "globals.h" + +extern FatVolume fatfs; +extern Adafruit_ImageReader *theImageReader; + +// CONFIGURATION FILE HANDLING --------------------------------------------- + +// This function decodes an integer value from the JSON config file in a +// variety of different formats...for example, "foo" might be specified: +// "foo" : 42 - As a signed decimal integer +// "foo" : "0x42" - Positive hexadecimal integer +// "foo" : "0xF800" - 16-bit RGB color +// "foo" : [ 255, 0, 0 ] - RGB color using integers 0-255 +// "foo" : [ "0xFF", "0x00", "0x00" ] - RGB using hexadecimal +// "foo" : [ 1.0, 0.0, 0.0 ] - RGB using floats +// 24-bit RGB colors will be decimated to 16-bit format. +// 16-bit colors returned by this func will be big-endian. +// Hexadecimal values MUST be quoted -- JSON can only handle hex as strings. +// This is NOT bulletproof! It does handle many well-formatted (and a few +// not-so-well-formatted) numbers, but not every imaginable case, and makes +// some guesses about what's an RGB color vs what isn't. Doing what I can, +// JSON is picky and and at some point folks just gotta get it together. +static int32_t dwim(JsonVariant v, int32_t def = 0) { // "Do What I Mean" + if(v.is()) { // If integer... + return v; // ...return value directly + } else if(v.is()) { // If float... + return (int)(v.as() + 0.5); // ...return rounded integer + } else if(v.is()) { // If string... + if((strlen(v) == 6) && !strncasecmp(v, "0x", 2)) { // 4-digit hex? + uint16_t rgb = strtol(v, NULL, 0); // Probably a 16-bit RGB color, + return __builtin_bswap16(rgb); // convert to big-endian + } else { + return strtol(v, NULL, 0); // Some other int/hex/octal + } + } else if(v.is()) { // If array... + if(v.size() >= 3) { // ...and at least 3 elements... + long cc[3]; // ...parse RGB color components... + for(uint8_t i=0; i<3; i++) { // Handle int/hex/octal/float... + if(v[i].is()) { + cc[i] = v[i].as(); + } else if(v[i].is()) { + cc[i] = (int)(v[i].as() * 255.999); + } else if(v[i].is()) { + cc[i] = strtol(v[i], NULL, 0); + } + if(cc[i] > 255) cc[i] = 255; // Clip to 8-bit range + else if(cc[i] < 0) cc[i] = 0; + } + uint16_t rgb = ((cc[0] & 0xF8) << 8) | // Decimate 24-bit RGB + ((cc[1] & 0xFC) << 3) | // to 16-bit + ( cc[2] >> 3); + return __builtin_bswap16(rgb); // and return big-endian + } else { // Some unexpected array + if(v[0].is()) { // Return first element + return v[0]; // as a simple integer, + } else { + return strtol(v[0], NULL, 0); // or int/hex/octal + } + } + } else { // Not found in document + return def; // ...return default value + } +} + +/* +static void getFilename(JsonVariant v, char **ptr) { + if(*ptr) { // If string already allocated, + free(*ptr); // delete old value... + *ptr = NULL; + } + if(v.is()) { + *ptr = strdup(v); // Make a copy of string, save that + } +} +*/ + +void loadConfig(char *filename) { + File file; + uint8_t rotation = 3; + + if(file = fatfs.open(filename)) { + StaticJsonDocument<2048> doc; + + yield(); + DeserializationError error = deserializeJson(doc, file); + yield(); + if(error) { + Serial.println("Config file error, using default settings"); + Serial.println(error.c_str()); + } else { + uint8_t e; + + // Values common to both eyes or global program config... + eyeRadius = dwim(doc["eyeRadius"]); + eyelidIndex = dwim(doc["eyelidIndex"]); + irisRadius = dwim(doc["irisRadius"]); + slitPupilRadius = dwim(doc["slitPupilRadius"]); + gazeMax = dwim(doc["gazeMax"], gazeMax); + JsonVariant v; + v = doc["coverage"]; + if(v.is() || v.is()) coverage = v.as(); + Serial.printf("Coverage: %f\n\r", coverage); + v = doc["upperEyelid"]; + if(v.is()) upperEyelidFilename = strdup(v); + Serial.printf("Upper Eyelid File: %s\n\r", upperEyelidFilename); + v = doc["lowerEyelid"]; + if(v.is()) lowerEyelidFilename = strdup(v); + Serial.printf("Lower Eyelid File: %s\n\r", lowerEyelidFilename); + + lightSensorMin = doc["lightSensorMin"] | lightSensorMin; + lightSensorMax = doc["lightSensorMax"] | lightSensorMax; + if(lightSensorMin > 1023) lightSensorMin = 1023; + else if(lightSensorMin < 0) lightSensorMin = 0; + if(lightSensorMax > 1023) lightSensorMax = 1023; + else if(lightSensorMax < 0) lightSensorMax = 0; + if(lightSensorMin > lightSensorMax) { + uint16_t temp = lightSensorMin; + lightSensorMin = lightSensorMax; + lightSensorMax = temp; + } + lightSensorCurve = doc["lightSensorCurve"] | lightSensorCurve; + if(lightSensorCurve < 0.01) lightSensorCurve = 0.01; + + // The pupil size is represented somewhat differently in the code + // than in the settings file. Expressing it as "pupilMin" (the + // smallest pupil size as a fraction of iris size, from 0.0 to 1.0) + // and pupilMax (the largest pupil size) seems easier for people + // to grasp. But in the code it's actually represented as irisMin + // (the inverse of pupilMax as described above) and irisRange + // (an amount added to irisMin which yields the inverse of pupilMin). + float pMax = doc["pupilMax"] | (1.0 - irisMin), + pMin = doc["pupilMin"] | (1.0 - (irisMin + irisRange)); + if(pMin > 1.0) pMin = 1.0; + else if(pMin < 0.0) pMin = 0.0; + if(pMax > 1.0) pMax = 1.0; + else if(pMax < 0.0) pMax = 0.0; + if(pMin > pMax) { + float temp = pMin; + pMin = pMax; + pMax = temp; + } + irisMin = (1.0 - pMax); + irisRange = (pMax - pMin); + + lightSensorPin = doc["lightSensor"] | lightSensorPin; + + + // Values that can be distinct per-eye but have a common default... + uint16_t pupilColor = dwim(doc["pupilColor"] , eye[0].pupilColor), + backColor = dwim(doc["backColor"] , eye[0].backColor), + irisColor = dwim(doc["irisColor"] , eye[0].iris.color), + scleraColor = dwim(doc["scleraColor"], eye[0].sclera.color), + irisMirror = 0, + scleraMirror = 0, + irisAngle = 0, + scleraAngle = 0, + irisiSpin = 0, + scleraiSpin = 0; + float irisSpin = 0.0, + scleraSpin = 0.0; + JsonVariant iristv = doc["irisTexture"], + scleratv = doc["scleraTexture"]; + + rotation = doc["rotate"] | rotation; // Screen rotation (GFX lib) + rotation &= 3; + + v = doc["tracking"]; + if(v.is()) tracking = v.as(); + v = doc["squint"]; + if(v.is()) { + trackFactor = 1.0 - v.as(); + if(trackFactor < 0.0) trackFactor = 0.0; + else if(trackFactor > 1.0) trackFactor = 1.0; + } + + // Convert clockwise int (0-1023) or float (0.0-1.0) values to CCW int used internally: + v = doc["irisSpin"]; + if(v.is()) irisSpin = v.as() * -1024.0; + v = doc["scleraSpin"]; + if(v.is()) scleraSpin = v.as() * -1024.0; + v = doc["irisiSpin"]; + if(v.is()) irisiSpin = v.as(); + v = doc["scleraiSpin"]; + if(v.is()) scleraiSpin = v.as(); + v = doc["irisMirror"]; + if(v.is() || v.is()) irisMirror = v ? 1023 : 0; + v = doc["scleraMirror"]; + if(v.is() || v.is()) scleraMirror = v ? 1023 : 0; + for(e=0; e()) irisAngle = 1023 - (v.as() & 1023); + else if(v.is()) irisAngle = 1023 - ((int)(v.as() * 1024.0) & 1023); + else irisAngle = eye[e].iris.angle; + eye[e].iris.angle = eye[e].iris.startAngle = irisAngle; + v = doc["scleraAngle"]; + if(v.is()) scleraAngle = 1023 - (v.as() & 1023); + else if(v.is()) scleraAngle = 1023 - ((int)(v.as() * 1024.0) & 1023); + else scleraAngle = eye[e].sclera.angle; + eye[e].sclera.angle = eye[e].sclera.startAngle = scleraAngle; + eye[e].iris.mirror = irisMirror; + eye[e].sclera.mirror = scleraMirror; + eye[e].iris.spin = irisSpin; + eye[e].sclera.spin = scleraSpin; + eye[e].iris.iSpin = irisiSpin; + eye[e].sclera.iSpin = scleraiSpin; + // iris and sclera filenames are strdup'd for each eye rather than + // sharing a common pointer, reason being that it gets really messy + // below when overriding one or the other and trying to do the right + // thing with free/strdup. So this does waste a tiny bit of RAM but + // it's only the size of the filenames and only during init. NBD. + if(iristv.is()) eye[e].iris.filename = strdup(iristv); + if(scleratv.is()) eye[e].sclera.filename = strdup(scleratv); + eye[e].rotation = rotation; // Might get override in per-eye code below + } + } + file.close(); + } else { + Serial.println("Can't open config file, using default settings"); + } + + // INITIALIZE DEFAULT VALUES if config file missing or in error ---------- + + // Some defaults are initialized in globals.h (because there's no way to + // check these for invalid input), others are initialized here if there's + // an obvious flag (e.g. value of 0 means "use default"). + + // Default eye size is set slightly larger than the screen. This is on + // purpose, because displacement effect looks worst at its extremes...this + // allows the pupil to move close to the edge of the display while keeping + // a few pixels distance from the displacement limits. + if(!eyeRadius) eyeRadius = DISPLAY_SIZE/2 + 5; + else eyeRadius = abs(eyeRadius); + eyeDiameter = eyeRadius * 2; + eyelidIndex &= 0xFF; // From table: learn.adafruit.com/assets/61921 + eyelidColor = eyelidIndex * 0x0101; // Expand eyelidIndex to 16-bit RGB + + if(!irisRadius) irisRadius = DISPLAY_SIZE/4; // Size in screen pixels + else irisRadius = abs(irisRadius); + slitPupilRadius = abs(slitPupilRadius); + if(slitPupilRadius > irisRadius) slitPupilRadius = irisRadius; + + if(coverage < 0.0) coverage = 0.0; + else if(coverage > 1.0) coverage = 1.0; + mapRadius = (int)(eyeRadius * M_PI * coverage + 0.5); + Serial.printf("Radius: %d\n\r", mapRadius); + mapDiameter = mapRadius * 2; + Serial.printf("Diam: %d\n\r", mapDiameter); +} + +// EYELID AND TEXTURE MAP FILE HANDLING ------------------------------------ + +// Load one eyelid, convert bitmap to 2 arrays (min, max values per column). +// Pass in filename, destination arrays (mins, maxes, 240 elements each). +ImageReturnCode loadEyelid(char *filename, + uint16_t *minArray, uint16_t *maxArray, uint16_t init) { + Adafruit_Image image; // Image object is on stack, pixel data is on heap + + yield(); + if (!theImageReader) { + Serial.println("No imagereader found"); + return IMAGE_ERR_FILE_NOT_FOUND; + } + + memset(minArray, init, DISPLAY_SIZE); // Fill eyelid arrays with init value + memset(maxArray, init, DISPLAY_SIZE); // mark 'no eyelid data for this column' + + yield(); + ImageReturnCode status; + if((status = theImageReader->loadBMP(filename, image)) == IMAGE_SUCCESS) { + Serial.println("Loaded image file"); + if(image.getFormat() == IMAGE_1) { // MUST be 1-bit image + Serial.printf("Eyelid loaded: (%d, %d)\n\r", image.width(), image.height()); + + uint16_t *palette = image.getPalette(); + uint8_t white = (!palette || (palette[1] > palette[0])); + int x, y, ix, iy, sx1, sx2, sy1, sy2; + // Center/clip eyelid image with respect to screen... + sx1 = (DISPLAY_SIZE - image.width()) / 2; // leftmost pixel, screen space + sy1 = (DISPLAY_SIZE - image.height()) / 2; // topmost pixel, screen space + sx2 = sx1 + image.width() - 1; // rightmost pixel, screen space + sy2 = sy1 + image.height() - 1; // lowest pixel, screen space + ix = -sx1; // leftmost pixel, image space + iy = -sy1; // topmost pixel, image space + if(sx1 < 0) sx1 = 0; // image wider than screen + if(sy1 < 0) sy1 = 0; // image taller than screen + if(sx2 > (DISPLAY_SIZE-1)) sx2 = DISPLAY_SIZE - 1; // image wider than screen + if(sy2 > (DISPLAY_SIZE-1)) sy2 = DISPLAY_SIZE - 1; // image taller than screen + if(ix < 0) ix = 0; // image narrower than screen + if(iy < 0) iy = 0; // image shorter than screen + + GFXcanvas1 *canvas = (GFXcanvas1 *)image.getCanvas(); + uint8_t *buffer = canvas->getBuffer(); + int bytesPerLine = (image.width() + 7) / 8; + for(x=sx1; x <= sx2; x++, ix++) { // For each column... + yield(); + // Get initial pointer into image buffer + uint8_t *ptr = &buffer[iy * bytesPerLine + ix / 8]; + uint8_t mask = 0x80 >> (ix & 7); // Column mask + uint8_t wbit = white ? mask : 0; // Bit value for white pixel + int miny = 65535, maxy; + for(y=sy1; y<=sy2; y++, ptr += bytesPerLine) { + if((*ptr & mask) == wbit) { // Is pixel set? + if(miny == 65535) miny = y; // If 1st set pixel, set miny + maxy = y; // If set pixel at all, set max + } + } + if(miny != 65535) { + // Because of coordinate system used later (screen rotated), + // min/max and Y coordinates are flipped before storing... + maxArray[x] = DISPLAY_SIZE - 1 - miny; + minArray[x] = DISPLAY_SIZE - 1 - maxy; + } + } + } else { + status = IMAGE_ERR_FORMAT; // Don't just return, need to dealloc... + } + } else { + Serial.println("Could not load file"); + } + + return status; +} + + +ImageReturnCode loadTexture(char *filename, uint16_t **data, + uint16_t *width, uint16_t *height) { + Adafruit_Image image; // Image object is on stack, pixel data is on heap + Serial.println("Loading texture"); + + yield(); + if (!theImageReader) { + Serial.println("No ImageReader found"); + return IMAGE_ERR_FILE_NOT_FOUND; + } + + yield(); + ImageReturnCode status; + if((status = theImageReader->loadBMP(filename, image)) == IMAGE_SUCCESS) { + Serial.println("Loaded image file"); + yield(); + if(image.getFormat() == IMAGE_16) { // MUST be 16-bit image + yield(); + GFXcanvas16 *canvas = (GFXcanvas16 *)image.getCanvas(); + //canvas->byteSwap(); // Match screen endianism for direct DMA xfer + Serial.printf("Texture loaded: (%d, %d)\n\r", image.width(), image.height()); + *width = image.width(); + *height = image.height(); + *data = (uint16_t *)malloc((int)*width * (int)*height * 2); + if (! data) { + *data = (uint16_t *)ps_malloc((int)*width * (int)*height * 2); + } + memcpy(*data, canvas->getBuffer(), (int)*width * (int)*height * 2); + } else { + Serial.printf("Could not load BMP file %s\n\r", filename); + status = IMAGE_ERR_FORMAT; // Don't just return, need to dealloc... + } + } else { + Serial.println("Could not load file"); + } + + return status; +} diff --git a/Qualia_ESP32S3_Eyes/globals.h b/Qualia_ESP32S3_Eyes/globals.h new file mode 100644 index 000000000..844a62489 --- /dev/null +++ b/Qualia_ESP32S3_Eyes/globals.h @@ -0,0 +1,166 @@ +// SPDX-FileCopyrightText: 2019 Phillip Burgess for Adafruit Industries +// +// SPDX-License-Identifier: MIT +#include +#include +#include +#include // Image-reading functions +#include + +//34567890123456789012345678901234567890123456789012345678901234567890123456 + +#if defined(GLOBAL_VAR) // #defined in .ino file ONLY! + #define GLOBAL_INIT(X) = (X) + #define INIT_EYESTRUCTS +#else + #define GLOBAL_VAR extern + #define GLOBAL_INIT(X) +#endif + +#define NUM_EYES 1 + +// GLOBAL VARIABLES -------------------------------------------------------- + +GLOBAL_VAR bool showSplashScreen GLOBAL_INIT(false); // Clear to suppress the splash screen + +#define MAX_DISPLAY_SIZE 480 +GLOBAL_VAR int DISPLAY_SIZE GLOBAL_INIT(240); // Start with assuming a 240x240 display +GLOBAL_VAR int eyeRadius GLOBAL_INIT(0); // 0 = Use default in loadConfig() +GLOBAL_VAR int eyeDiameter; // Calculated from eyeRadius later +GLOBAL_VAR int irisRadius GLOBAL_INIT(60); // Approx size in screen pixels +GLOBAL_VAR int slitPupilRadius GLOBAL_INIT(0); // 0 = round pupil +GLOBAL_VAR uint8_t eyelidIndex GLOBAL_INIT(0x00); // From table: learn.adafruit.com/assets/61921 +GLOBAL_VAR uint16_t eyelidColor GLOBAL_INIT(0x0000); // Expand eyelidIndex to 16-bit +// mapRadius is the size of one quadrant of the polar-to-rectangular map, +// in pixels. To cover the front hemisphere of the eye, this should be a +// minimum of (eyeRadius * Pi / 2) -- but, to provide some coverage beyond +// just the front hemisphere, the value of 'coverage' determines how far +// this map wraps around the eye. 0.0 = no coverage, 0.5 = front hemisphere, +// 1.0 = full sphere. Do not bother making this 1.0 -- the far back side of +// the eye is never actually seen, since we're using a displacement map hack +// and not actually rotating a sphere, plus the resulting map would take a +// TON of RAM, probably more than we have. The default here, 0.6, provides +// a good balance between coverage and RAM, only occasionally will you see +// a crescent of back-of-eye color (and the sclera texture map can be +// designed to blend into it). eyeRadius is calculated in loadConfig() as +// eyeRadius * Pi * coverage -- if eyeRadius is 125 and coverage is 0.6, +// mapRadius will be 236 pixels, and the resulting polar angle/dist maps +// will total about 111K RAM. +GLOBAL_VAR float coverage GLOBAL_INIT(0.6); +GLOBAL_VAR int mapRadius; // calculated in loadConfig() +GLOBAL_VAR int mapDiameter; // calculated in loadConfig() +GLOBAL_VAR uint16_t *displace GLOBAL_INIT(NULL); +GLOBAL_VAR uint8_t *polarAngle GLOBAL_INIT(NULL); +GLOBAL_VAR int8_t *polarDist GLOBAL_INIT(NULL); +GLOBAL_VAR uint16_t upperOpen[MAX_DISPLAY_SIZE]; +GLOBAL_VAR uint16_t upperClosed[MAX_DISPLAY_SIZE]; +GLOBAL_VAR uint16_t lowerOpen[MAX_DISPLAY_SIZE]; +GLOBAL_VAR uint16_t lowerClosed[MAX_DISPLAY_SIZE]; +GLOBAL_VAR char *upperEyelidFilename GLOBAL_INIT(NULL); +GLOBAL_VAR char *lowerEyelidFilename GLOBAL_INIT(NULL); +GLOBAL_VAR uint16_t lightSensorMin GLOBAL_INIT(0); +GLOBAL_VAR uint16_t lightSensorMax GLOBAL_INIT(1023); +GLOBAL_VAR float lightSensorCurve GLOBAL_INIT(1.0); +GLOBAL_VAR float irisMin GLOBAL_INIT(0.45); +GLOBAL_VAR float irisRange GLOBAL_INIT(0.35); +GLOBAL_VAR bool tracking GLOBAL_INIT(true); +GLOBAL_VAR float trackFactor GLOBAL_INIT(0.5); +GLOBAL_VAR uint32_t gazeMax GLOBAL_INIT(3000000); // Max wait time (uS) for major eye movements + +// Random eye motion: provided by the base project, but overridable by user code. +GLOBAL_VAR bool moveEyesRandomly GLOBAL_INIT(true); // Clear to suppress random eye motion and let user code control it +GLOBAL_VAR float eyeTargetX GLOBAL_INIT(0.0); // Then set these continuously in user_loop. +GLOBAL_VAR float eyeTargetY GLOBAL_INIT(0.0); // Range is from -1.0 to +1.0. + +// Pin definition stuff will go here + +GLOBAL_VAR int8_t lightSensorPin GLOBAL_INIT(-1); +GLOBAL_VAR int8_t blinkPin GLOBAL_INIT(-1); // Manual both-eyes blink pin (-1 = none) + + +// EYE-RELATED STRUCTURES -------------------------------------------------- + +// 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; + +// Data for iris and sclera texture maps +typedef struct { + char *filename; + float spin; // RPM * 1024.0 + uint16_t color; + uint16_t *data; + uint16_t width; + uint16_t height; + uint16_t startAngle; // INITIAL rotation 0-1023 CCW + uint16_t angle; // CURRENT rotation 0-1023 CCW + uint16_t mirror; // 0 = normal, 1023 = flip X axis + uint16_t iSpin; // Per-frame fixed integer spin, overrides 'spin' value +} texture; + +// Each eye then uses the following structure. Each eye must be on its own +// SPI bus with distinct control lines (unlike the Uncanny Eyes code where +// they take turns on one bus). Two of the column structures as described +// above, then a lot of DMA nitty-gritty and animation state data. +typedef struct { + // These first values are initialized in the tables below: + const char *name; // For loading per-eye configurables + int8_t winkPin; // Manual eye wink control (-1 = none) + + Arduino_RGB_Display *display; + // Remaining values are initialized in code: + uint16_t pupilColor; // 16-bit 565 RGB, big-endian + uint16_t backColor; // 16-bit 565 RGB, big-endian + texture iris; // iris texture map + texture sclera; // sclera texture map + uint8_t rotation; // Screen rotation (GFX lib) + + + uint16_t column[MAX_DISPLAY_SIZE]; + uint16_t colNum; + + // Stuff carried over from Uncanny Eyes code. It now needs to be + // independent per-eye because we interleave between drawing the + // two eyes scanline-by-line rather than drawing each eye in full. + // This'll likely get cleaned up a little, but for now... + eyeBlink blink; + float eyeX, eyeY; // Save per-eye to avoid tearing + float pupilFactor; // ditto + float blinkFactor; + float upperLidFactor, lowerLidFactor; +} eyeStruct; + +#ifdef INIT_EYESTRUCTS + eyeStruct eye[NUM_EYES] = { + { NULL, -1 } }; +#else + extern eyeStruct eye[]; +#endif + +// FUNCTION PROTOTYPES ----------------------------------------------------- + +// Functions in file.cpp +extern int file_setup(bool msc=true); +extern void handle_filesystem_change(); +// This is set true when filesystem contents have changed. +// Set true initially so the program starts with the "changed" task. +extern bool filesystem_change_flag GLOBAL_INIT(true); +extern void loadConfig(char *filename); +extern ImageReturnCode loadEyelid(char *filename, uint16_t *minArray, uint16_t *maxArray, uint16_t init); +extern ImageReturnCode loadTexture(char *filename, uint16_t **data, uint16_t *width, uint16_t *height); + +// Functions in tablegen.cpp +extern void calcDisplacement(void); +extern void calcMap(void); +extern float screen2map(int in); +extern float map2screen(int in); + +// Functions in user.cpp +extern void user_setup(void); +extern void user_loop(void); diff --git a/Qualia_ESP32S3_Eyes/main.cpp b/Qualia_ESP32S3_Eyes/main.cpp new file mode 100644 index 000000000..10dff3697 --- /dev/null +++ b/Qualia_ESP32S3_Eyes/main.cpp @@ -0,0 +1,863 @@ +// SPDX-FileCopyrightText: 2019 Phillip Burgess for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +// Animated eyes for Adafruit MONSTER M4SK and HALLOWING M4 dev boards. +// This code is pretty tightly coupled to the resources of these boards +// (one or two ST7789 240x240 pixel TFTs on separate SPI buses, and a +// SAMD51 microcontroller), and not as generally portable as the prior +// "Uncanny Eyes" project (better for SAMD21 chips or Teensy 3.X and +// 128x128 TFT or OLED screens, single SPI bus). + +// IMPORTANT: in rare situations, a board may get "bricked" when running +// this code while simultaneously connected to USB. A quick-flashing status +// LED indicates the filesystem has gone corrupt. If this happens, install +// CircuitPython to reinitialize the filesystem, copy over your eye files +// (keep backups!), then re-upload this code. It seems to happen more often +// at high optimization settings (above -O3), but there's not 1:1 causality. +// The exact cause has not yet been found...possibly insufficient yield() +// calls, or some rare alignment in the Arcada library or USB-handling code. + +// LET'S HAVE A WORD ABOUT COORDINATE SYSTEMS before continuing. From an +// outside observer's point of view, looking at the display(s) on these +// boards, the eyes are rendered COLUMN AT A TIME, working LEFT TO RIGHT, +// rather than the horizontal scanline order of Uncanny Eyes and most other +// graphics-heavy code. It was found much easier to animate the eyelids when +// working along this axis. A "column major" display is easily achieved by +// setting the screen(s) to ROTATION 3, which is a 90 degree +// counterclockwise rotation relative to the default. This places (0,0) at +// the BOTTOM-LEFT of the display, with +X being UP and +Y being RIGHT -- +// so, conceptually, just swapping axes you have a traditional Cartesian +// coordinate system and trigonometric functions work As Intended, and the +// code tends to "think" that way in most places. Since the rotation is done +// in hardware though...from the display driver's point of view, one might +// think of these as "horizontal" "scanlines," and that the eye is being +// drawn sideways, with a left and right eyelid rather than bottom and top. +// Just mentioning it here because there may still be lingering comments +// and/or variables in the code where I refer to "scanlines" even though +// visually/spatially these are columns. Will do my best to comment local +// coordinate systems in different spots. (Any raster images loaded by +// Adafruit_ImageReader are referenced in typical +Y = DOWN order.) + +// Oh also, "left eye" and "right eye" refer to the MONSTER'S left and +// right. From an observer's point of view, looking AT the monster, the +// "right eye" is on the left. + + +#define GLOBAL_VAR +#include "globals.h" + + +Adafruit_FlashTransport_ESP32 flashTransport; +Adafruit_SPIFlash flash(&flashTransport); +FatVolume fatfs; + +Adafruit_ImageReader *theImageReader = NULL; + +Arduino_XCA9554SWSPI *expander = new Arduino_XCA9554SWSPI( + PCA_TFT_RESET, PCA_TFT_CS, PCA_TFT_SCK, PCA_TFT_MOSI, + &Wire, 0x3F); + +Arduino_ESP32RGBPanel *rgbpanel = new Arduino_ESP32RGBPanel( + TFT_DE, TFT_VSYNC, TFT_HSYNC, TFT_PCLK, + TFT_R1, TFT_R2, TFT_R3, TFT_R4, TFT_R5, + TFT_G0, TFT_G1, TFT_G2, TFT_G3, TFT_G4, TFT_G5, + TFT_B1, TFT_B2, TFT_B3, TFT_B4, TFT_B5, + 1 /* hsync_polarity */, 50 /* hsync_front_porch */, 2 /* hsync_pulse_width */, 44 /* hsync_back_porch */, + 1 /* vsync_polarity */, 16 /* vsync_front_porch */, 2 /* vsync_pulse_width */, 18 /* vsync_back_porch */ + //,0, 6000000 + ); + +Arduino_RGB_Display *gfx = new Arduino_RGB_Display( +// 480 /* width */, 480 /* height */, rgbpanel, 0 /* rotation */, true /* auto_flush */, +// expander, GFX_NOT_DEFINED /* RST */, tl034wvs05_b1477a_init_operations, sizeof(tl034wvs05_b1477a_init_operations)); + 480 /* width */, 480 /* height */, rgbpanel, 0 /* rotation */, true /* auto_flush */, + expander, GFX_NOT_DEFINED /* RST */, TL021WVC02_init_operations, sizeof(TL021WVC02_init_operations)); +// 480 /* width */, 480 /* height */, rgbpanel, 0 /* rotation */, true /* auto_flush */, +// expander, GFX_NOT_DEFINED /* RST */, TL028WVC01_init_operations, sizeof(TL028WVC01_init_operations)); +// 720 /* width */, 720 /* height */, rgbpanel, 0 /* rotation */, true /* auto_flush */, +// expander, GFX_NOT_DEFINED /* RST */, NULL, 0); + + +// Global eye state that applies to all eyes (not per-eye): +bool eyeInMotion = false; +float eyeOldX, eyeOldY, eyeNewX, eyeNewY; +uint32_t eyeMoveStartTime = 0L; +int32_t eyeMoveDuration = 0L; +uint32_t lastSaccadeStop = 0L; +int32_t saccadeInterval = 0L; + +// Some sloppy eye state stuff, some carried over from old eye code... +// kinda messy and badly named and will get cleaned up/moved/etc. +uint32_t timeOfLastBlink = 0L, + timeToNextBlink = 0L; +int xPositionOverMap = 0; +int yPositionOverMap = 0; +uint8_t eyeNum = 0; +uint32_t frames = 0; +uint32_t lastFrameRateReportTime = 0; +uint32_t lastLightReadTime = 0; +float lastLightValue = 0.5; +double irisValue = 0.5; +int iPupilFactor = 42; +int fixate = 7; +uint8_t lightSensorFailCount = 0; + +// For autonomous iris scaling +#define IRIS_LEVELS 7 +float iris_prev[IRIS_LEVELS] = { 0 }; +float iris_next[IRIS_LEVELS] = { 0 }; +uint16_t iris_frame = 0; + +uint8_t scaling = 1; + +// Crude error handler. Prints message to Serial Monitor, blinks LED. +void fatal(const char *message, uint16_t blinkDelay) { + Serial.begin(115200); + Serial.println(message); + while (1) yield(); +} + +#include // sbrk() function + + +// SETUP FUNCTION - CALLED ONCE AT PROGRAM START --------------------------- + +void setup() { + Serial.begin(115200); + //while (!Serial) { delay(100); } + Serial.println("Adafruit Qualia Eyes"); + + // Initialize flash library and check its chip ID. + if (!flash.begin()) { + Serial.println("Error, failed to initialize flash chip!"); + while (1) delay(10); + } + Serial.printf("Flash chip JEDEC ID: 0x%04X \n\r", flash.getJEDECID()); + // to make sure the filesystem was mounted. + if (!fatfs.begin(&flash)) { + Serial.println("Failed to mount filesystem!"); + Serial.println("Was CircuitPython loaded on the board first to create the " + "filesystem?"); + while (1) delay(10); + } + Serial.println("Mounted filesystem."); + // Print out filesystem + fatfs.ls("/", LS_R | LS_DATE | LS_SIZE); + theImageReader = new Adafruit_ImageReader(fatfs); + + user_setup(); + + +#ifdef GFX_EXTRA_PRE_INIT + GFX_EXTRA_PRE_INIT(); +#endif + + Serial.println("Beginning"); + // Init Display + + Wire.setClock(400000); // speed up I2C + if (!gfx->begin()) { + Serial.println("gfx->begin() failed!"); + while (1) delay(10); + } + gfx->fillScreen(BLUE); + + expander->pinMode(PCA_TFT_BACKLIGHT, OUTPUT); + expander->digitalWrite(PCA_TFT_BACKLIGHT, HIGH); + + DISPLAY_SIZE = min(gfx->width(), gfx->height()); + if (DISPLAY_SIZE > 480) { + DISPLAY_SIZE /= 2; // pixel doubling! + scaling = 2; + } + // No file selector yet. In the meantime, you can override the default + // config file by holding one of the 3 edge buttons at startup (loads + // config1.eye, config2.eye or config3.eye instead). Keep fingers clear + // of the nose booper when doing this...it self-calibrates on startup. + // DO THIS BEFORE THE SPLASH SO IT DOESN'T REQUIRE A LENGTHY HOLD. + char *filename = (char *)"config.eye"; + + expander->pinMode(PCA_BUTTON_UP, INPUT); + expander->pinMode(PCA_BUTTON_DOWN, INPUT); + + if((! expander->digitalRead(PCA_BUTTON_DOWN)) && fatfs.exists("config1.eye")) { + filename = (char *)"config1.eye"; + } else if((! expander->digitalRead(PCA_BUTTON_UP)) && fatfs.exists("config2.eye")) { + filename = (char *)"config2.eye"; + } + + + yield(); + // Initialize display(s) + eye[0].display = gfx; + + uint8_t e; + for(e=0; efillScreen(0); + + // Default settings that can be overridden in config file + eye[e].pupilColor = 0x0000; + eye[e].backColor = 0xFFFF; + eye[e].iris.color = 0xFF01; + eye[e].iris.data = NULL; + eye[e].iris.filename = NULL; + eye[e].iris.startAngle = (e & 1) ? 512 : 0; // Rotate alternate eyes 180 degrees + eye[e].iris.angle = eye[e].iris.startAngle; + eye[e].iris.mirror = 0; + eye[e].iris.spin = 0.0; + eye[e].iris.iSpin = 0; + eye[e].sclera.color = 0xFFFF; + eye[e].sclera.data = NULL; + eye[e].sclera.filename = NULL; + eye[e].sclera.startAngle = (e & 1) ? 512 : 0; // Rotate alternate eyes 180 degrees + eye[e].sclera.angle = eye[e].sclera.startAngle; + eye[e].sclera.mirror = 0; + eye[e].sclera.spin = 0.0; + eye[e].sclera.iSpin = 0; + eye[e].rotation = 3; + + // Uncanny eyes carryover stuff for now, all messy: + eye[e].blink.state = NOBLINK; + eye[e].blinkFactor = 0.0; + } + // SPLASH SCREEN (IF FILE PRESENT) --------------------------------------- + + yield(); + uint32_t startTime, elapsed; + if (showSplashScreen) { + /* + showSplashScreen = ((theImageReader.drawBMP((char *)"/splash.bmp", + 0, 0, eye[0].display)) == IMAGE_SUCCESS); + if (showSplashScreen) { // Loaded OK? + Serial.println("Splashing"); + if (NUM_EYES > 1) { // Load on other eye too, ignore status + yield(); + theImageReader.drawBMP((char *)"/splash.bmp", 0, 0, eye[1].display); + } + expander->digitalWrite(PCA_TFT_BACKLIGHT, HIGH); + startTime = millis(); // Note current time for backlight hold later + } + */ + } + + // If no splash, or load failed, turn backlight on early so user gets a + // little feedback, that the board is not locked up, just thinking. + if (!showSplashScreen) expander->digitalWrite(PCA_TFT_BACKLIGHT, HIGH); + + // LOAD CONFIGURATION FILE ----------------------------------------------- + + loadConfig(filename); + + // LOAD EYELIDS AND TEXTURE MAPS ----------------------------------------- + + // Load texture maps for eyes + uint8_t e2; + for(e=0; e= e)) { // If first eye, or no match found... + // If no iris filename was specified, or if file fails to load... + if((eye[e].iris.filename == NULL) || (loadTexture(eye[e].iris.filename, + &eye[e].iris.data, &eye[e].iris.width, &eye[e].iris.height) != IMAGE_SUCCESS)) { + // Point iris data at the color variable and set image size to 1px + eye[e].iris.data = &eye[e].iris.color; + eye[e].iris.width = eye[e].iris.height = 1; + Serial.printf("Iris load texture fail: %s\n\r", eye[e].iris.filename); + } + } + // Repeat for sclera... + for(e2=0; e2= e)) { // If first eye, or no match found... + // If no sclera filename was specified, or if file fails to load... + if((eye[e].sclera.filename == NULL) || (loadTexture(eye[e].sclera.filename, + &eye[e].sclera.data, &eye[e].sclera.width, &eye[e].sclera.height) != IMAGE_SUCCESS)) { + // Point sclera data at the color variable and set image size to 1px + eye[e].sclera.data = &eye[e].sclera.color; + eye[e].sclera.width = eye[e].sclera.height = 1; + Serial.printf("Sclera load texture fail: %s\n\r", eye[e].sclera.filename); + } + } + } + // Load eyelid graphics. + yield(); + ImageReturnCode status; + + status = loadEyelid(upperEyelidFilename ? + upperEyelidFilename : (char *)"upper.bmp", + upperClosed, upperOpen, DISPLAY_SIZE-1); + if (status != IMAGE_SUCCESS) { + Serial.println("Upper eyelid load fail"); + } + + // print out contents of upperclosed & upperopen + for (int i = 0; i < DISPLAY_SIZE; i++) { + Serial.printf("upperclosed[%d] = %d\tupperopen[%d] = %d\n\r", i, upperClosed[i], i, upperOpen[i]); + } + + status = loadEyelid(lowerEyelidFilename ? + lowerEyelidFilename : (char *)"lower.bmp", + lowerOpen, lowerClosed, 0); + if (status != IMAGE_SUCCESS) { + Serial.println("Lower eyelid load fail"); + } + Serial.println("Loaded eyelids"); + + calcMap(); + calcDisplacement(); + + randomSeed(analogRead(A0)); + eyeOldX = eyeNewX = eyeOldY = eyeNewY = mapRadius; // Start in center + for(e=0; esetRotation(eye[e].rotation); // MEME FIX + eye[e].eyeX = eyeOldX; // Set up initial position + eye[e].eyeY = eyeOldY; + } + + if (showSplashScreen) { // Image(s) loaded above? + // Hold backlight on for up to 2 seconds (minus other initialization time) + if ((elapsed = (millis() - startTime)) < 2000) { + delay(2000 - elapsed); + } + expander->digitalWrite(PCA_TFT_BACKLIGHT, LOW); + for(e=0; efillScreen(0); + eye[e].colNum = 0; + } + } + + expander->digitalWrite(PCA_TFT_BACKLIGHT, HIGH); // Back on, impending graphics + + yield(); + + lastLightReadTime = micros() + 2000000; // Delay initial light reading + + pinMode(SCK, OUTPUT); + pinMode(MOSI, OUTPUT); + pinMode(MISO, OUTPUT); + pinMode(SS, OUTPUT); +} + + +// LOOP FUNCTION - CALLED REPEATEDLY UNTIL POWER-OFF ----------------------- + +/* +The loop() function in this code is a weird animal, operating a bit +differently from the earlier "Uncanny Eyes" eye project. Whereas in the +prior project we did this: + + for(each eye) { + * do position calculations, etc. for one frame of animation * + for(each scanline) { + * draw a row of pixels * + } + } + +This new code works "inside out," more like this: + + for(each column) { + if(first column of display) { + * do position calculations, etc. for one frame of animation * + } + * draw a column of pixels * + } + +The reasons for this are that A) we have an INORDINATE number of pixels to +draw compared to the old project (nearly 4X as much), and B) each screen is +now on its own SPI bus...data can be issued concurrently...so, rather than +stalling in a while() loop waiting for each scanline transfer to complete +(just wasting cycles), the code looks for opportunities to work on other +eyes (the eye updates aren't necessarily synchronized; each can function at +an independent frame rate depending on particular complexity at the moment). +*/ + +// LOOP FUNCTION - CALLED REPEATEDLY UNTIL POWER-OFF ----------------------- + +/* +The loop() function in this code is a weird animal, operating a bit +differently from the earlier "Uncanny Eyes" eye project. Whereas in the +prior project we did this: + + for(each eye) { + * do position calculations, etc. for one frame of animation * + for(each scanline) { + * draw a row of pixels * + } + } + +This new code works "inside out," more like this: + + for(each column) { + if(first column of display) { + * do position calculations, etc. for one frame of animation * + } + * draw a column of pixels * + } + +The reasons for this are that A) we have an INORDINATE number of pixels to +draw compared to the old project (nearly 4X as much), and B) each screen is +now on its own SPI bus...data can be issued concurrently...so, rather than +stalling in a while() loop waiting for each scanline transfer to complete +(just wasting cycles), the code looks for opportunities to work on other +eyes (the eye updates aren't necessarily synchronized; each can function at +an independent frame rate depending on particular complexity at the moment). +*/ + +// loop() function processes ONE COLUMN of ONE EYE... +uint32_t timestamp = 0; + +void loop() { + uint16_t column[MAX_DISPLAY_SIZE]; + if(++eyeNum >= NUM_EYES) eyeNum = 0; // Cycle through eyes... + + uint16_t x = eye[eyeNum].colNum; + uint32_t t = micros(); + //Serial.println(x); + if(!x) { // If it's the first column... + digitalWrite(SS, HIGH); + Serial.printf("Timestamp: %d ms\n\r", millis() - timestamp); + timestamp = millis(); + // ONCE-PER-FRAME EYE ANIMATION LOGIC HAPPENS HERE ------------------- + + // Eye movement + float eyeX, eyeY; + if(moveEyesRandomly) { + int32_t dt = t - eyeMoveStartTime; // uS elapsed since last eye event + if(eyeInMotion) { // Eye currently moving? + if(dt >= eyeMoveDuration) { // Time up? Destination reached. + eyeInMotion = false; // Stop moving + // The "move" duration temporarily becomes a hold duration... + // Normally this is 35 ms to 1 sec, but don't exceed gazeMax setting + uint32_t limit = min((uint32_t)1000000, gazeMax); + eyeMoveDuration = random(35000, limit); // Time between microsaccades + if(!saccadeInterval) { // Cleared when "big" saccade finishes + lastSaccadeStop = t; // Time when saccade stopped + saccadeInterval = random(eyeMoveDuration, gazeMax); // Next in 30ms to 3sec + } + // Similarly, the "move" start time becomes the "stop" starting time... + eyeMoveStartTime = t; // Save time of event + eyeX = eyeOldX = eyeNewX; // Save position + eyeY = eyeOldY = eyeNewY; + } else { // Move time's not yet fully elapsed -- interpolate position + float e = (float)dt / float(eyeMoveDuration); // 0.0 to 1.0 during move + e = 3 * e * e - 2 * e * e * e; // Easing function: 3*e^2-2*e^3 0.0 to 1.0 + eyeX = eyeOldX + (eyeNewX - eyeOldX) * e; // Interp X + eyeY = eyeOldY + (eyeNewY - eyeOldY) * e; // and Y + } + } else { // Eye is currently stopped + eyeX = eyeOldX; + eyeY = eyeOldY; + if(dt > eyeMoveDuration) { // Time up? Begin new move. + if((t - lastSaccadeStop) > saccadeInterval) { // Time for a "big" saccade + // r is the radius in X and Y that the eye can go, from (0,0) in the center. + float r = ((float)mapDiameter - (float)DISPLAY_SIZE * M_PI_2) * 0.75; + Serial.printf("mapDiameter: %d, DISPLAY_SIZE: %d, r: %f\n\r", mapDiameter, DISPLAY_SIZE, r); + eyeNewX = random(-r, r); + float h = sqrt(r * r - eyeNewX * eyeNewX); + eyeNewY = random(-h, h); + // Set the duration for this move, and start it going. + eyeMoveDuration = random(83000, 166000); // ~1/12 - ~1/6 sec + saccadeInterval = 0; // Calc next interval when this one stops + } else { // Microsaccade + // r is possible radius of motion, ~1/10 size of full saccade. + // We don't bother with clipping because if it strays just a little, + // that's okay, it'll get put in-bounds on next full saccade. + float r = (float)mapDiameter - (float)DISPLAY_SIZE * M_PI_2; + r *= 0.07; + float dx = random(-r, r); + eyeNewX = eyeX - mapRadius + dx; + float h = sqrt(r * r - dx * dx); + eyeNewY = eyeY - mapRadius + random(-h, h); + eyeMoveDuration = random(7000, 25000); // 7-25 ms microsaccade + } + eyeNewX += mapRadius; // Translate new point into map space + eyeNewY += mapRadius; + eyeMoveStartTime = t; // Save initial time of move + eyeInMotion = true; // Start move on next frame + } + } + } else { + // Allow user code to control eye position (e.g. IR sensor, joystick, etc.) + float r = ((float)mapDiameter - (float)DISPLAY_SIZE * M_PI_2) * 0.9; + eyeX = mapRadius + eyeTargetX * r; + eyeY = mapRadius + eyeTargetY * r; + } + + // Eyes fixate (are slightly crossed) -- amount is filtered for boops + int nufix = 7; + fixate = ((fixate * 15) + nufix) / 16; + // save eye position to this eye's struct so it's same throughout render + if(eyeNum & 1) eyeX += fixate; // Eyes converge slightly toward center + else eyeX -= fixate; + eye[eyeNum].eyeX = eyeX; + eye[eyeNum].eyeY = eyeY; + + //Serial.printf("eye location: (%f, %f)\n\r", eyeX, eyeY); + + // pupilFactor? irisValue? TO DO: pick a name and stick with it + eye[eyeNum].pupilFactor = irisValue; + // Also note - irisValue is calculated at the END of this function + // for the next frame (because the sensor must be read when there's + // no SPI traffic to the left eye) + + // 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 upperOpen[ix]) { + uq = 1.0; + } else if(iy < upperClosed[ix]) { + uq = 0.0; + } else { + uq = (float)(iy - upperClosed[ix]) / (float)(upperOpen[ix] - upperClosed[ix]); + } + lq = 1.0 - uq; + } else { + // If no tracking, eye is FULLY OPEN when not blinking + uq = 1.0; + lq = 1.0; + } + // Dampen eyelid movements slightly + // SAVE upper & lower lid factors per eye, + // they need to stay consistent across frame + eye[eyeNum].upperLidFactor = (eye[eyeNum].upperLidFactor * 0.6) + (uq * 0.4); + eye[eyeNum].lowerLidFactor = (eye[eyeNum].lowerLidFactor * 0.6) + (lq * 0.4); + + // Process blinks + if(eye[eyeNum].blink.state) { // Eye currently blinking? + // Check if current blink state time has elapsed + if((t - eye[eyeNum].blink.startTime) >= eye[eyeNum].blink.duration) { + if(++eye[eyeNum].blink.state > DEBLINK) { // Deblinking finished? + eye[eyeNum].blink.state = NOBLINK; // No longer blinking + eye[eyeNum].blinkFactor = 0.0; + } else { // Advancing from ENBLINK to DEBLINK mode + eye[eyeNum].blink.duration *= 2; // DEBLINK is 1/2 ENBLINK speed + eye[eyeNum].blink.startTime = t; + eye[eyeNum].blinkFactor = 1.0; + } + } else { + eye[eyeNum].blinkFactor = (float)(t - eye[eyeNum].blink.startTime) / (float)eye[eyeNum].blink.duration; + if(eye[eyeNum].blink.state == DEBLINK) eye[eyeNum].blinkFactor = 1.0 - eye[eyeNum].blinkFactor; + } + } + + // Periodically report frame rate. Really this is "total number of + // eyeballs drawn." If there are two eyes, the overall refresh rate + // of both screens is about 1/2 this. + frames++; + if(((t - lastFrameRateReportTime) >= 1000000) && t) { // Once per sec. + Serial.println((frames * 1000) / (t / 1000)); + lastFrameRateReportTime = t; + } + + float mins = (float)millis() / 60000.0; + if(eye[eyeNum].iris.iSpin) { + // Spin works in fixed amount per frame (eyes may lose sync, but "wagon wheel" tricks work) + eye[eyeNum].iris.angle += eye[eyeNum].iris.iSpin; + } else { + // Keep consistent timing in spin animation (eyes stay in sync, no "wagon wheel" effects) + eye[eyeNum].iris.angle = (int)((float)eye[eyeNum].iris.startAngle + eye[eyeNum].iris.spin * mins + 0.5); + } + if(eye[eyeNum].sclera.iSpin) { + eye[eyeNum].sclera.angle += eye[eyeNum].sclera.iSpin; + } else { + eye[eyeNum].sclera.angle = (int)((float)eye[eyeNum].sclera.startAngle + eye[eyeNum].sclera.spin * mins + 0.5); + } + digitalWrite(SS, LOW); + // END ONCE-PER-FRAME EYE ANIMATION ---------------------------------- + + } // end first-scanline check + // PER-COLUMN RENDERING ------------------------------------------------ + + digitalWrite(SCK, HIGH); + // Should be possible for these to be local vars, + // but the animation becomes super chunky then, what gives? + xPositionOverMap = (int)(eye[eyeNum].eyeX - (DISPLAY_SIZE/2.0)); + yPositionOverMap = (int)(eye[eyeNum].eyeY - (DISPLAY_SIZE/2.0)); + + // These are constant across frame and could be stored in eye struct + float upperLidFactor = (1.0 - eye[eyeNum].blinkFactor) * eye[eyeNum].upperLidFactor, + lowerLidFactor = (1.0 - eye[eyeNum].blinkFactor) * eye[eyeNum].lowerLidFactor; + iPupilFactor = (int)((float)eye[eyeNum].iris.height * 256 * (1.0 / eye[eyeNum].pupilFactor)); + + int y1, y2; + int lidColumn = (eyeNum & 1) ? (DISPLAY_SIZE - 1 - x) : x; // Reverse eyelid columns for left eye + + // Render column 'x' into eye's next available renderBuf + uint16_t *ptr = column; //eye[eyeNum].column; + memset(ptr, 0x0, DISPLAY_SIZE * 2); // Fill with background color + + if(upperOpen[lidColumn] == 65535) { + // No eyelid data for this line; eyelid image is smaller than screen. + // Great! Make a full scanline of nothing, no rendering needed: + } else { + y1 = lowerClosed[lidColumn] + (int)(0.5 + lowerLidFactor * + (float)((int)lowerOpen[lidColumn] - (int)lowerClosed[lidColumn])); + y2 = upperClosed[lidColumn] + (int)(0.5 + upperLidFactor * + (float)((int)upperOpen[lidColumn] - (int)upperClosed[lidColumn])); + if(y1 > DISPLAY_SIZE-1) y1 = DISPLAY_SIZE-1; // Clip results in case lidfactor + else if(y1 < 0) y1 = 0; // is beyond the usual 0.0 to 1.0 range + if(y2 > DISPLAY_SIZE-1) y2 = DISPLAY_SIZE-1; + else if(y2 < 0) y2 = 0; + + //Serial.printf("Eye opening from %d to %d\n\r", y1, y2); + + if(y1 >= y2) { + // Eyelid is fully or partially closed, enough that there are no + // pixels to be rendered for this line. Make "nothing," as above. + } else { + // If single eye, dynamically build descriptor list as needed, + // else use a single descriptor & fully buffer each line. + + int xx = xPositionOverMap + x; + int y; + for(y=0; y= 0) && (mx < mapDiameter) && (my >= 0) && (my < mapDiameter)) { + // Inside polar angle/dist map + int angle, dist, moff; + if(my >= mapRadius) { + if(mx >= mapRadius) { // Quadrant 1 + // Use angle & dist directly + mx -= mapRadius; + my -= mapRadius; + moff = my * mapRadius + mx; // Offset into map arrays + angle = polarAngle[moff]; + dist = polarDist[moff]; + } else { // Quadrant 2 + // ROTATE angle by 90 degrees (270 degrees clockwise; 768) + // MIRROR dist on X axis + mx = mapRadius - 1 - mx; + my -= mapRadius; + angle = polarAngle[mx * mapRadius + my] + 768; + dist = polarDist[ my * mapRadius + mx]; + } + } else { + if(mx < mapRadius) { // Quadrant 3 + // ROTATE angle by 180 degrees + // MIRROR dist on X & Y axes + mx = mapRadius - 1 - mx; + my = mapRadius - 1 - my; + moff = my * mapRadius + mx; + angle = polarAngle[moff] + 512; + dist = polarDist[ moff]; + } else { // Quadrant 4 + // ROTATE angle by 270 degrees (90 degrees clockwise; 256) + // MIRROR dist on Y axis + mx -= mapRadius; + my = mapRadius - 1 - my; + angle = polarAngle[mx * mapRadius + my] + 256; + dist = polarDist[ my * mapRadius + mx]; + } + } + // Convert angle/dist to texture map coords + if(dist >= 0) { // Sclera + angle = ((angle + eye[eyeNum].sclera.angle) & 1023) ^ eye[eyeNum].sclera.mirror; + int tx = angle * eye[eyeNum].sclera.width / 1024; // Texture map x/y + int ty = dist * eye[eyeNum].sclera.height / 128; + *ptr++ = eye[eyeNum].sclera.data[ty * eye[eyeNum].sclera.width + tx]; + } else if(dist > -128) { // Iris or pupil + int ty = dist * iPupilFactor / -32768; + if(ty >= eye[eyeNum].iris.height) { // Pupil + *ptr++ = eye[eyeNum].pupilColor; + } else { // Iris + angle = ((angle + eye[eyeNum].iris.angle) & 1023) ^ eye[eyeNum].iris.mirror; + int tx = angle * eye[eyeNum].iris.width / 1024; + *ptr++ = eye[eyeNum].iris.data[ty * eye[eyeNum].iris.width + tx]; + } + } else { + *ptr++ = eye[eyeNum].backColor; // Back of eye + } + } else { + *ptr++ = eye[eyeNum].backColor; // Off map, use back-of-eye color + } + } else { // Outside eyeball area + *ptr++ = eyelidColor; + } + } + digitalWrite(MOSI, LOW); + + for(; ygetFramebuffer(); + uint16_t colnum = eye[eyeNum].colNum; + if (scaling == 1) { + memcpy(framebuf+(colnum * DISPLAY_SIZE * 2), column, DISPLAY_SIZE * 2); + } else if (scaling == 2) { + uint16_t scaled[DISPLAY_SIZE * 2]; + + for(int i = 0; i < DISPLAY_SIZE; i++) { + uint16_t pixel = column[i]; + scaled[i * 2] = pixel; + scaled[i * 2 + 1] = pixel; + } + memcpy(framebuf + ((2*colnum) * DISPLAY_SIZE * 4), scaled, DISPLAY_SIZE * 4); + memcpy(framebuf + ((2*colnum + 1) * DISPLAY_SIZE * 4), scaled, DISPLAY_SIZE * 4); + } + //gfx->draw16bitRGBBitmap(0, eye[eyeNum].colNum, column, DISPLAY_SIZE, 1); + + // At this point, above checks confirm that column is ready and DMA is free + if(!x) { // If it's the first column... + if(eyeNum == (NUM_EYES-1)) { + // Handle pupil scaling + if(lightSensorPin >= 0) { + // Read light sensor, but not too often (Seesaw hates that) + #define LIGHT_INTERVAL (1000000 / 10) // 10 Hz, don't poll Seesaw too often + if((t - lastLightReadTime) >= LIGHT_INTERVAL) { + // Fun fact: eyes have a "consensual response" to light -- both + // pupils will react even if the opposite eye is stimulated. + // Meaning we can get away with using a single light sensor for + // both eyes. This comment has nothing to do with the code. + uint16_t rawReading = analogRead(lightSensorPin); + if(rawReading <= 1023) { + if(rawReading < lightSensorMin) rawReading = lightSensorMin; // Clamp light sensor range + else if(rawReading > lightSensorMax) rawReading = lightSensorMax; // to within usable range + float v = (float)(rawReading - lightSensorMin) / (float)(lightSensorMax - lightSensorMin); // 0.0 to 1.0 + v = pow(v, lightSensorCurve); + lastLightValue = irisMin + v * irisRange; + lastLightReadTime = t; + lightSensorFailCount = 0; + } + } + irisValue = (irisValue * 0.97) + (lastLightValue * 0.03); // Filter response for smooth reaction + } else { + // Not light responsive. Use autonomous iris w/fractal subdivision + float n, sum = 0.5; + for(uint16_t i=0; i iris min/max + if((++iris_frame) >= (1 << IRIS_LEVELS)) iris_frame = 0; + } + user_loop(); + } + } + if(++eye[eyeNum].colNum >= DISPLAY_SIZE) { // If last line sent... + eye[eyeNum].colNum = 0; // Wrap to beginning + } + digitalWrite(MISO, LOW); +} + +/* +void loop() +{ + Serial.println("Hello!"); + gfx->fillScreen(BLACK); + gfx->setCursor(0, gfx->height() / 2 - 75); + gfx->setTextSize(5); + gfx->setTextColor(WHITE); + gfx->setTextWrap(false); + gfx->println("Hello World 123"); + + gfx->setCursor(0, gfx->height() / 2 - 25); + gfx->setTextColor(RED); + gfx->println("RED"); + + gfx->setCursor(0, gfx->height() / 2 + 25); + gfx->setTextColor(GREEN); + gfx->println("GREEN"); + + gfx->setCursor(00, gfx->height() / 2 + 75); + gfx->setTextColor(BLUE); + gfx->println("BLUE"); + + delay(3000); + + gfx->fillScreen(RED); + delay(500); + gfx->fillScreen(GREEN); + delay(500); + gfx->fillScreen(BLUE); + delay(500); + gfx->fillScreen(WHITE); + delay(500); +}*/ \ No newline at end of file diff --git a/Qualia_ESP32S3_Eyes/tablegen.cpp b/Qualia_ESP32S3_Eyes/tablegen.cpp new file mode 100644 index 000000000..2c3663e95 --- /dev/null +++ b/Qualia_ESP32S3_Eyes/tablegen.cpp @@ -0,0 +1,159 @@ +// SPDX-FileCopyrightText: 2019 Phillip Burgess for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +//34567890123456789012345678901234567890123456789012345678901234567890123456 + +#include "globals.h" + +// Code in this file calculates various tables used in eye rendering. + +// Because 3D math is probably asking too much of our microcontroller, +// the round eyeball shape is faked using a 2D displacement map, a la +// Photoshop's displacement filter or old demoscene & screensaver tricks. +// This is not really an accurate representation of 3D rotation, +// but works well enough for fooling the casual observer. + +void calcDisplacement() { + // To save RAM, the displacement map is calculated for ONE QUARTER of + // the screen, then mirrored horizontally/vertically down the middle + // when rendering. Additionally, only a single axis displacement need + // be calculated, since eye shape is X/Y symmetrical one can just swap + // axes to look up displacement on the opposing axis. + if(displace = (uint16_t *)malloc((DISPLAY_SIZE/2) * (DISPLAY_SIZE/2))) { + float eyeRadius2 = (float)(eyeRadius * eyeRadius); + uint16_t x, y; + float dx, dy, d2, d, h, a, pa; + uint16_t *ptr = displace; + // Displacement is calculated for the first quadrant in traditional + // "+Y is up" Cartesian coordinate space; any mirroring or rotation + // is handled in eye rendering code. + for(y=0; y<(DISPLAY_SIZE/2); y++) { + yield(); // Periodic yield() makes sure mass storage filesystem stays alive + dy = (float)y + 0.5; + dy *= dy; // Now dy^2 + for(x=0; x<(DISPLAY_SIZE/2); x++) { + // Get distance to origin point. Pixel centers are at +0.5, this is + // normal, desirable and by design -- screen center at (120.0,120.0) + // falls between pixels and allows numerically-correct mirroring. + dx = (float)x + 0.5; + d2 = dx * dx + dy; // Distance to origin, squared + if(d2 <= eyeRadius2) { // Pixel is within eye area + d = sqrt(d2); // Distance to origin + h = sqrt(eyeRadius2 - d2); // Height of eye hemisphere at d + a = atan2(d, h); // Angle from center: 0 to pi/2 + //pa = a * eyeRadius; // Convert to pixels (no) + pa = a / M_PI_2 * mapRadius; // Convert to pixels + dx /= d; // Normalize dx part of 2D vector + *ptr++ = (uint8_t)(dx * pa) - x; // Round to pixel space (no +0.5) + } else { // Outside eye area + *ptr++ = 65535; // Mark as out-of-eye-bounds + } + } + } + } +} + +void calcMap(void) { + int pixels = mapRadius * mapRadius; + if(polarAngle = (uint8_t *)malloc(pixels * 2)) { // Single alloc for both tables + polarDist = (int8_t *)&polarAngle[pixels]; // Offset to second table + + // CALCULATE POLAR ANGLE & DISTANCE + + float mapRadius2 = mapRadius * mapRadius; // Radius squared + float iRad = screen2map(irisRadius); // Iris size in in polar map pixels + float irisRadius2 = iRad * iRad; // Iris size squared + + uint8_t *anglePtr = polarAngle; + int8_t *distPtr = polarDist; + + // Like the displacement map, only the first quadrant is calculated, + // and the other three quadrants are mirrored/rotated from this. + int x, y; + float dx, dy, dy2, d2, d, angle, xp; + for(y=0; y mapRadius2) { // If it exceeds 1/2 map size, squared, + *anglePtr++ = 0; // then mark as out-of-eye-bounds + *distPtr++ = -128; + } else { // else pixel is within eye area... + angle = atan2(dy, dx); // -pi to +pi (0 to +pi/2 in 1st quadrant) + angle = M_PI_2 - angle; // Clockwise, 0 at top + angle *= 512.0 / M_PI; // 0 to <256 in 1st quadrant + *anglePtr++ = (uint8_t)angle; + d = sqrt(d2); + if(d2 > irisRadius2) { + // Point is in sclera + d = (mapRadius - d) / (mapRadius - iRad); + d *= 127.0; + *distPtr++ = (int8_t)d; // 0 to 127 + } else { + // Point is in iris (-dist to indicate such) + d = (iRad - d) / iRad; + d *= -127.0; + *distPtr++ = (int8_t)d - 1; // -1 to -127 + } + } + } + } + + // If slit pupil is enabled, override iris area of polarDist map. + if(slitPupilRadius > 0) { + // Iterate over each pixel in the iris section of the polar map... + for(y=0; y < mapRadius; y++) { + yield(); // Periodic yield() makes sure mass storage filesystem stays alive + dy = y + 0.5; // Distance to center, Y component + dy2 = dy * dy; + for(x=0; x < mapRadius; x++) { + dx = x + 0.5; // Distance to center point, X component + d2 = dx * dx + dy2; // Distance to center, squared + if(d2 <= irisRadius2) { // If inside iris... + yield(); + xp = x + 0.5; + // This is a bit ugly in that it iteratively calculates the + // polarDist value...trial and error. It should be possible to + // algebraically simplify this and find the single polarDist + // point for a given pixel, but I've not worked that out yet. + // This is only needed once at startup, not a complete disaster. + for(int i=126; i>=0; i--) { + float ratio = i / 128.0; // 0.0 (open) to just-under-1.0 (slit) (>= 1.0 will cause trouble) + // Interpolate a point between top of iris and top of slit pupil, based on ratio + float y1 = iRad - (iRad - slitPupilRadius) * ratio; + // (x1 is 0 and thus dropped from equation below) + // And another point between right of iris and center of eye, inverse ratio + float x2 = iRad * (1.0 - ratio); + // (y2 is also zero, same deal) + // Find X coordinate of center of circle that crosses above two points + // and has Y at 0.0 + float xc = (x2 * x2 - y1 * y1) / (2 * x2); + dx = x2 - xc; // Distance from center of circle to right edge + float r2 = dx * dx; // center-to-right distance squared + dx = xp - xc; // X component of... + d2 = dx * dx + dy2; // Distance from pixel to left 'xc' point + if(d2 <= r2) { // If point is within circle... + polarDist[y * mapRadius + x] = (int8_t)(-1 - i); // Set to distance 'i' + break; + } + } + } + } + } + } + } +} + +// Scale a measurement in screen pixels to polar map pixels +float screen2map(int in) { + return atan2(in, sqrt(eyeRadius * eyeRadius - in * in)) / M_PI_2 * mapRadius; +} + +// Inverse of above +float map2screen(int in) { + return sin((float)in / (float)mapRadius) * M_PI_2 * eyeRadius; +} diff --git a/Qualia_ESP32S3_Eyes/user.cpp b/Qualia_ESP32S3_Eyes/user.cpp new file mode 100644 index 000000000..8065ed3cb --- /dev/null +++ b/Qualia_ESP32S3_Eyes/user.cpp @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2019 Phillip Burgess for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#if 1 // Change to 0 to disable this code (must enable ONE user*.cpp only!) + +#include "globals.h" + +// This file provides a crude way to "drop in" user code to the eyes, +// allowing concurrent operations without having to maintain a bunch of +// special derivatives of the eye code (which is still undergoing a lot +// of development). Just replace the source code contents of THIS TAB ONLY, +// compile and upload to board. Shouldn't need to modify other eye code. + +// User globals can go here, recommend declaring as static, e.g.: +// static int foo = 42; + +// Called once near the end of the setup() function. If your code requires +// a lot of time to initialize, make periodic calls to yield() to keep the +// USB mass storage filesystem alive. +void user_setup(void) { +} + +// Called periodically during eye animation. This is invoked in the +// interval before starting drawing on the last eye (left eye on MONSTER +// M4SK, sole eye on HalloWing M0) so it won't exacerbate visible tearing +// in eye rendering. This is also SPI "quiet time" on the MONSTER M4SK so +// it's OK to do I2C or other communication across the bridge. +// This function BLOCKS, it does NOT multitask with the eye animation code, +// and performance here will have a direct impact on overall refresh rates, +// so keep it simple. Avoid loops (e.g. if animating something like a servo +// or NeoPixels in response to some trigger) and instead rely on state +// machines or similar. Additionally, calls to this function are NOT time- +// constant -- eye rendering time can vary frame to frame, so animation or +// other over-time operations won't look very good using simple +/- +// increments, it's better to use millis() or micros() and work +// algebraically with elapsed times instead. +void user_loop(void) { +/* + Suppose we have a global bool "animating" (meaning something is in + motion) and global uint32_t's "startTime" (the initial time at which + something triggered movement) and "transitionTime" (the total time + over which movement should occur, expressed in microseconds). + Maybe it's servos, maybe NeoPixels, or something different altogether. + This function might resemble something like (pseudocode): + + if(!animating) { + Not in motion, check sensor for trigger... + if(read some sensor) { + Motion is triggered! Record startTime, set transition + to 1.5 seconds and set animating flag: + startTime = micros(); + transitionTime = 1500000; + animating = true; + No motion actually takes place yet, that will begin on + the next pass through this function. + } + } else { + Currently in motion, ignore trigger and move things instead... + uint32_t elapsed = millis() - startTime; + if(elapsed < transitionTime) { + Part way through motion...how far along? + float ratio = (float)elapsed / (float)transitionTime; + Do something here based on ratio, 0.0 = start, 1.0 = end + } else { + End of motion reached. + Take whatever steps here to move into final position (1.0), + and then clear the "animating" flag: + animating = false; + } + } +*/ +} + +#endif // 0