Adafruit_Learning_System_Gu.../M4_Eyes/file.cpp
2020-09-26 12:35:47 -04:00

457 lines
20 KiB
C++

//34567890123456789012345678901234567890123456789012345678901234567890123456
#define ARDUINOJSON_ENABLE_COMMENTS 1
#include <ArduinoJson.h> // JSON config file functions
#include "globals.h"
extern Adafruit_Arcada arcada;
// 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<int>()) { // If integer...
return v; // ...return value directly
} else if(v.is<float>()) { // If float...
return (int)(v.as<float>() + 0.5); // ...return rounded integer
} else if(v.is<char*>()) { // 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<JsonArray>()) { // 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<int>()) {
cc[i] = v[i].as<int>();
} else if(v[i].is<float>()) {
cc[i] = (int)(v[i].as<float>() * 255.999);
} else if(v[i].is<char*>()) {
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<int>()) { // 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<char*>()) {
*ptr = strdup(v); // Make a copy of string, save that
}
}
*/
void loadConfig(char *filename) {
File file;
uint8_t rotation = 3;
if(file = arcada.open(filename, FILE_READ)) {
StaticJsonDocument<2048> doc;
yield();
// delay(100); // Make sure mass storage handler has a turn first!
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...
stackReserve = dwim(doc["stackReserve"], stackReserve),
eyeRadius = dwim(doc["eyeRadius"]);
eyelidIndex = dwim(doc["eyelidIndex"]);
irisRadius = dwim(doc["irisRadius"]);
slitPupilRadius = dwim(doc["slitPupilRadius"]);
JsonVariant v;
v = doc["coverage"];
if(v.is<int>() || v.is<float>()) coverage = v.as<float>();
v = doc["upperEyelid"];
if(v.is<char*>()) upperEyelidFilename = strdup(v);
v = doc["lowerEyelid"];
if(v.is<char*>()) lowerEyelidFilename = strdup(v);
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;
boopPin = doc["boopSensor"] | boopPin;
// Computed at startup, NOT from file now
// boopThreshold = doc["boopThreshold"] | boopThreshold;
// 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<bool>()) tracking = v.as<bool>();
v = doc["squint"];
if(v.is<float>()) {
trackFactor = 1.0 - v.as<float>();
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<float>()) irisSpin = v.as<float>() * -1024.0;
v = doc["scleraSpin"];
if(v.is<float>()) scleraSpin = v.as<float>() * -1024.0;
v = doc["irisiSpin"];
if(v.is<int>()) irisiSpin = v.as<int>();
v = doc["scleraiSpin"];
if(v.is<int>()) scleraiSpin = v.as<int>();
v = doc["irisMirror"];
if(v.is<bool>() || v.is<int>()) irisMirror = v ? 1023 : 0;
v = doc["scleraMirror"];
if(v.is<bool>() || v.is<int>()) scleraMirror = v ? 1023 : 0;
for(e=0; e<NUM_EYES; e++) {
eye[e].pupilColor = pupilColor;
eye[e].backColor = backColor;
eye[e].iris.color = irisColor;
eye[e].sclera.color = scleraColor;
// The globally-set irisAngle and scleraAngle are read each
// time through because each eye has a distinct default if
// not set globally. Override only if set globally at first...
v = doc["irisAngle"];
if(v.is<int>()) irisAngle = 1023 - (v.as<int>() & 1023);
else if(v.is<float>()) irisAngle = 1023 - ((int)(v.as<float>() * 1024.0) & 1023);
else irisAngle = eye[e].iris.angle;
eye[e].iris.angle = eye[e].iris.startAngle = irisAngle;
v = doc["scleraAngle"];
if(v.is<int>()) scleraAngle = 1023 - (v.as<int>() & 1023);
else if(v.is<float>()) scleraAngle = 1023 - ((int)(v.as<float>() * 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<char*>()) eye[e].iris.filename = strdup(iristv);
if(scleratv.is<char*>()) eye[e].sclera.filename = strdup(scleratv);
eye[e].rotation = rotation; // Might get override in per-eye code below
}
#if NUM_EYES > 1
// Process any distinct per-eye settings...
// NOT EVERYTHING IS CONFIGURABLE PER-EYE. Color and texture stuff, yes.
// Other things like iris size or pupil shape are not, reason being that
// there isn't enough RAM for the polar angle/dist tables for two eyes.
for(uint8_t e=0; e<NUM_EYES; e++) {
eye[e].pupilColor = dwim(doc[eye[e].name]["pupilColor"] , eye[e].pupilColor);
eye[e].backColor = dwim(doc[eye[e].name]["backColor"] , eye[e].backColor);
eye[e].iris.color = dwim(doc[eye[e].name]["irisColor"] , eye[e].iris.color);
eye[e].sclera.color = dwim(doc[eye[e].name]["scleraColor"] , eye[e].sclera.color);
v = doc[eye[e].name]["irisAngle"];
if(v.is<int>()) eye[e].iris.angle = 1023 - (v.as<int>() & 1023);
else if(v.is<float>()) eye[e].iris.angle = 1023 - ((int)(v.as<float>() * 1024.0) & 1023);
eye[e].iris.startAngle = eye[e].iris.angle;
v = doc[eye[e].name]["scleraAngle"];
if(v.is<int>()) eye[e].sclera.angle = 1023 - (v.as<int>() & 1023);
else if(v.is<float>()) eye[e].sclera.angle = 1023 - ((int)(v.as<float>() * 1024.0) & 1023);
eye[e].sclera.startAngle = eye[e].sclera.angle;
v = doc[eye[e].name]["irisSpin"];
if(v.is<float>()) eye[e].iris.spin = v.as<float>() * -1024.0;
v = doc[eye[e].name]["scleraSpin"];
if(v.is<float>()) eye[e].sclera.spin = v.as<float>() * -1024.0;
v = doc[eye[e].name]["irisiSpin"];
if(v.is<int>()) eye[e].iris.iSpin = v.as<int>();
v = doc[eye[e].name]["scleraiSpin"];
if(v.is<int>()) eye[e].sclera.iSpin = v.as<int>();
v = doc[eye[e].name]["irisMirror"];
if(v.is<bool>() || v.is<int>()) eye[e].iris.mirror = v ? 1023 : 0;
v = doc[eye[e].name]["scleraMirror"];
if(v.is<bool>() || v.is<int>()) eye[e].sclera.mirror = v ? 1023 : 0;
v = doc[eye[e].name]["irisTexture"];
if(v.is<char*>()) { // Per-eye iris texture specified?
if(eye[e].iris.filename) free(eye[e].iris.filename); // Remove old name if any
eye[e].iris.filename = strdup(v); // Save new name
}
v = doc[eye[e].name]["scleraTexture"]; // Ditto w/sclera
if(v.is<char*>()) {
if(eye[e].sclera.filename) free(eye[e].sclera.filename);
eye[e].sclera.filename = strdup(v);
}
eye[e].rotation = doc[eye[e].name]["rotate"] | rotation;
eye[e].rotation &= 3;
}
#endif
#if defined(ADAFRUIT_MONSTER_M4SK_EXPRESS)
v = doc["voice"];
if(v.is<bool>()) voiceOn = v.as<bool>();
currentPitch = defaultPitch = doc["pitch"] | defaultPitch;
gain = doc["gain"] | gain;
modulate = doc["modulate"] | modulate;
v = doc["waveform"];
if(v.is<char*>()) { // If string...
if(!strncasecmp( v, "sq", 2)) waveform = 1;
else if(!strncasecmp(v, "si", 2)) waveform = 2;
else if(!strncasecmp(v, "t" , 1)) waveform = 3;
else if(!strncasecmp(v, "sa", 2)) waveform = 4;
else waveform = 0;
}
#endif // ADAFRUIT_MONSTER_M4SK_EXPRESS
}
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);
mapDiameter = mapRadius * 2;
}
// 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,
uint8_t *minArray, uint8_t *maxArray, uint8_t init, uint32_t maxRam) {
Adafruit_Image image; // Image object is on stack, pixel data is on heap
int32_t w, h;
uint32_t tempBytes;
uint8_t *tempPtr = NULL;
ImageReturnCode status;
Adafruit_ImageReader *reader;
reader = arcada.getImageReader();
if (!reader) {
return IMAGE_ERR_FILE_NOT_FOUND;
}
memset(minArray, init, DISPLAY_SIZE); // Fill eyelid arrays with init value to
memset(maxArray, init, DISPLAY_SIZE); // mark 'no eyelid data for this column'
// This is the "booster seat" described in m4eyes.ino
if(reader->bmpDimensions(filename, &w, &h) == IMAGE_SUCCESS) {
tempBytes = ((w + 7) / 8) * h; // Bitmap size in bytes
if (maxRam > tempBytes) {
if((tempPtr = (uint8_t *)malloc(maxRam - tempBytes)) != NULL) {
// Make SOME tempPtr reference, or optimizer removes the alloc!
tempPtr[0] = 0;
}
}
// DON'T nest the image-reading case in here. If the fragmentation
// culprit can be found, this block of code (and the free() block
// later) are easily removed.
}
yield();
if((status = reader->loadBMP(filename, image)) == IMAGE_SUCCESS) {
if(image.getFormat() == IMAGE_1) { // MUST be 1-bit image
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...
// 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 = 255, maxy;
for(y=sy1; y<=sy2; y++, ptr += bytesPerLine) {
if((*ptr & mask) == wbit) { // Is pixel set?
if(miny == 255) miny = y; // If 1st set pixel, set miny
maxy = y; // If set pixel at all, set max
}
}
if(miny != 255) {
// 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...
}
}
if(tempPtr) {
free(tempPtr);
}
// image destructor will handle dealloc of that object's data
return status;
}
ImageReturnCode loadTexture(char *filename, uint16_t **data,
uint16_t *width, uint16_t *height, uint32_t maxRam) {
Adafruit_Image image; // Image object is on stack, pixel data is on heap
int32_t w, h;
uint32_t tempBytes;
uint8_t *tempPtr = NULL;
ImageReturnCode status;
Adafruit_ImageReader *reader;
reader = arcada.getImageReader();
if (!reader) {
return IMAGE_ERR_FILE_NOT_FOUND;
}
// This is the "booster seat" described in m4eyes.ino
if(reader->bmpDimensions(filename, &w, &h) == IMAGE_SUCCESS) {
tempBytes = w * h * 2; // Image size in bytes (converted to 16bpp)
if (maxRam > tempBytes) {
if((tempPtr = (uint8_t *)malloc(maxRam - tempBytes)) != NULL) {
// Make SOME tempPtr reference, or optimizer removes the alloc!
tempPtr[0] = 0;
}
}
// DON'T nest the image-reading case in here. If the fragmentation
// culprit can be found, this block of code (and the free() block
// later) are easily removed.
}
yield();
if((status = reader->loadBMP(filename, image)) == IMAGE_SUCCESS) {
if(image.getFormat() == IMAGE_16) { // MUST be 16-bit image
Serial.println("Texture loaded!");
GFXcanvas16 *canvas = (GFXcanvas16 *)image.getCanvas();
canvas->byteSwap(); // Match screen endianism for direct DMA xfer
*width = image.width();
*height = image.height();
*data = (uint16_t *)arcada.writeDataToFlash((uint8_t *)canvas->getBuffer(),
(int)*width * (int)*height * 2);
} else {
status = IMAGE_ERR_FORMAT; // Don't just return, need to dealloc...
}
}
if(tempPtr) {
free(tempPtr);
}
// image destructor will handle dealloc of that object's data
return status;
}