/ M4_Eyes / file.cpp
file.cpp
  1  // SPDX-FileCopyrightText: 2019 Phillip Burgess for Adafruit Industries
  2  //
  3  // SPDX-License-Identifier: MIT
  4  
  5  //34567890123456789012345678901234567890123456789012345678901234567890123456
  6  
  7  #define ARDUINOJSON_ENABLE_COMMENTS 1
  8  #include <ArduinoJson.h>          // JSON config file functions
  9  #include "globals.h"
 10  
 11  extern Adafruit_Arcada arcada;
 12  
 13  // CONFIGURATION FILE HANDLING ---------------------------------------------
 14  
 15  // This function decodes an integer value from the JSON config file in a
 16  // variety of different formats...for example, "foo" might be specified:
 17  // "foo" : 42                         - As a signed decimal integer
 18  // "foo" : "0x42"                     - Positive hexadecimal integer
 19  // "foo" : "0xF800"                   - 16-bit RGB color
 20  // "foo" : [ 255, 0, 0 ]              - RGB color using integers 0-255
 21  // "foo" : [ "0xFF", "0x00", "0x00" ] - RGB using hexadecimal
 22  // "foo" : [ 1.0, 0.0, 0.0 ]          - RGB using floats
 23  // 24-bit RGB colors will be decimated to 16-bit format.
 24  // 16-bit colors returned by this func will be big-endian.
 25  // Hexadecimal values MUST be quoted -- JSON can only handle hex as strings.
 26  // This is NOT bulletproof! It does handle many well-formatted (and a few
 27  // not-so-well-formatted) numbers, but not every imaginable case, and makes
 28  // some guesses about what's an RGB color vs what isn't. Doing what I can,
 29  // JSON is picky and and at some point folks just gotta get it together.
 30  static int32_t dwim(JsonVariant v, int32_t def = 0) { // "Do What I Mean"
 31    if(v.is<int>()) {                      // If integer...
 32      return v;                            // ...return value directly
 33    } else if(v.is<float>()) {             // If float...
 34      return (int)(v.as<float>() + 0.5);   // ...return rounded integer
 35    } else if(v.is<char*>()) {             // If string...
 36      if((strlen(v) == 6) && !strncasecmp(v, "0x", 2)) { // 4-digit hex?
 37        uint16_t rgb = strtol(v, NULL, 0); // Probably a 16-bit RGB color,
 38        return __builtin_bswap16(rgb);     // convert to big-endian
 39      } else {
 40        return strtol(v, NULL, 0);         // Some other int/hex/octal
 41      }
 42    } else if(v.is<JsonArray>()) {         // If array...
 43      if(v.size() >= 3) {                  // ...and at least 3 elements...
 44        long cc[3];                        // ...parse RGB color components...
 45        for(uint8_t i=0; i<3; i++) {       // Handle int/hex/octal/float...
 46          if(v[i].is<int>()) {
 47            cc[i] = v[i].as<int>();
 48          } else if(v[i].is<float>()) {
 49            cc[i] = (int)(v[i].as<float>() * 255.999);
 50          } else if(v[i].is<char*>()) {
 51            cc[i] = strtol(v[i], NULL, 0);
 52          }
 53          if(cc[i] > 255)    cc[i] = 255;  // Clip to 8-bit range
 54          else if(cc[i] < 0) cc[i] = 0;
 55        }
 56        uint16_t rgb = ((cc[0] & 0xF8) << 8) | // Decimate 24-bit RGB
 57                       ((cc[1] & 0xFC) << 3) | // to 16-bit
 58                       ( cc[2]         >> 3);
 59        return __builtin_bswap16(rgb);         // and return big-endian
 60      } else {                             // Some unexpected array
 61        if(v[0].is<int>()) {               // Return first element
 62          return v[0];                     // as a simple integer,
 63        } else {
 64          return strtol(v[0], NULL, 0);    // or int/hex/octal
 65        }
 66      }
 67    } else {                               // Not found in document
 68      return def;                          // ...return default value
 69    }
 70  }
 71  
 72  /*
 73  static void getFilename(JsonVariant v, char **ptr) {
 74    if(*ptr) {          // If string already allocated,
 75      free(*ptr);       // delete old value...
 76      *ptr = NULL;
 77    }
 78    if(v.is<char*>()) {
 79      *ptr = strdup(v); // Make a copy of string, save that
 80    }
 81  }
 82  */
 83  
 84  void loadConfig(char *filename) {
 85    File    file;
 86    uint8_t rotation = 3;
 87  
 88    if(file = arcada.open(filename, FILE_READ)) {
 89      StaticJsonDocument<2048> doc;
 90  
 91      yield();
 92      DeserializationError error = deserializeJson(doc, file);
 93      yield();
 94      if(error) {
 95        Serial.println("Config file error, using default settings");
 96        Serial.println(error.c_str());
 97      } else {
 98        uint8_t e;
 99  
100        // Values common to both eyes or global program config...
101        stackReserve    = dwim(doc["stackReserve"], stackReserve),
102        eyeRadius       = dwim(doc["eyeRadius"]);
103        eyelidIndex     = dwim(doc["eyelidIndex"]);
104        irisRadius      = dwim(doc["irisRadius"]);
105        slitPupilRadius = dwim(doc["slitPupilRadius"]);
106        gazeMax         = dwim(doc["gazeMax"], gazeMax);
107        JsonVariant v;
108        v = doc["coverage"];
109        if(v.is<int>() || v.is<float>()) coverage = v.as<float>();
110        v = doc["upperEyelid"];
111        if(v.is<char*>())    upperEyelidFilename = strdup(v);
112        v = doc["lowerEyelid"];
113        if(v.is<char*>())    lowerEyelidFilename = strdup(v);
114  
115        lightSensorMin   = doc["lightSensorMin"] | lightSensorMin;
116        lightSensorMax   = doc["lightSensorMax"] | lightSensorMax;
117        if(lightSensorMin > 1023)   lightSensorMin = 1023;
118        else if(lightSensorMin < 0) lightSensorMin = 0;
119        if(lightSensorMax > 1023)   lightSensorMax = 1023;
120        else if(lightSensorMax < 0) lightSensorMax = 0;
121        if(lightSensorMin > lightSensorMax) {
122          uint16_t  temp = lightSensorMin;
123          lightSensorMin = lightSensorMax;
124          lightSensorMax = temp;
125        }
126        lightSensorCurve = doc["lightSensorCurve"] | lightSensorCurve;
127        if(lightSensorCurve < 0.01) lightSensorCurve = 0.01;
128  
129        // The pupil size is represented somewhat differently in the code
130        // than in the settings file. Expressing it as "pupilMin" (the
131        // smallest pupil size as a fraction of iris size, from 0.0 to 1.0)
132        // and pupilMax (the largest pupil size) seems easier for people
133        // to grasp. But in the code it's actually represented as irisMin
134        // (the inverse of pupilMax as described above) and irisRange
135        // (an amount added to irisMin which yields the inverse of pupilMin).
136        float pMax = doc["pupilMax"] | (1.0 - irisMin),
137              pMin = doc["pupilMin"] | (1.0 - (irisMin + irisRange));
138        if(pMin > 1.0)      pMin = 1.0;
139        else if(pMin < 0.0) pMin = 0.0;
140        if(pMax > 1.0)      pMax = 1.0;
141        else if(pMax < 0.0) pMax = 0.0;
142        if(pMin > pMax) {
143          float temp = pMin;
144          pMin = pMax;
145          pMax = temp;
146        }
147        irisMin   = (1.0 - pMax);
148        irisRange = (pMax - pMin);
149  
150        lightSensorPin = doc["lightSensor"]   | lightSensorPin;
151        boopPin        = doc["boopSensor"]    | boopPin;
152  // Computed at startup, NOT from file now
153  //      boopThreshold  = doc["boopThreshold"] | boopThreshold;
154  
155        // Values that can be distinct per-eye but have a common default...
156        uint16_t    pupilColor   = dwim(doc["pupilColor"] , eye[0].pupilColor),
157                    backColor    = dwim(doc["backColor"]  , eye[0].backColor),
158                    irisColor    = dwim(doc["irisColor"]  , eye[0].iris.color),
159                    scleraColor  = dwim(doc["scleraColor"], eye[0].sclera.color),
160                    irisMirror   = 0,
161                    scleraMirror = 0,
162                    irisAngle    = 0,
163                    scleraAngle  = 0,
164                    irisiSpin    = 0,
165                    scleraiSpin  = 0;
166        float       irisSpin     = 0.0,
167                    scleraSpin   = 0.0;
168        JsonVariant iristv       = doc["irisTexture"],
169                    scleratv     = doc["scleraTexture"];
170  
171        rotation  = doc["rotate"] | rotation; // Screen rotation (GFX lib)
172        rotation &= 3;
173  
174        v = doc["tracking"];
175        if(v.is<bool>()) tracking = v.as<bool>();
176        v = doc["squint"];
177        if(v.is<float>()) {
178          trackFactor = 1.0 - v.as<float>();
179          if(trackFactor < 0.0)      trackFactor = 0.0;
180          else if(trackFactor > 1.0) trackFactor = 1.0;
181        }
182  
183        // Convert clockwise int (0-1023) or float (0.0-1.0) values to CCW int used internally:
184        v = doc["irisSpin"];
185        if(v.is<float>()) irisSpin   = v.as<float>() * -1024.0;
186        v = doc["scleraSpin"];
187        if(v.is<float>()) scleraSpin = v.as<float>() * -1024.0;
188        v = doc["irisiSpin"];
189        if(v.is<int>()) irisiSpin    = v.as<int>();
190        v = doc["scleraiSpin"];
191        if(v.is<int>()) scleraiSpin  = v.as<int>();
192        v = doc["irisMirror"];
193        if(v.is<bool>() || v.is<int>()) irisMirror   = v ? 1023 : 0;
194        v = doc["scleraMirror"];
195        if(v.is<bool>() || v.is<int>()) scleraMirror = v ? 1023 : 0;
196        for(e=0; e<NUM_EYES; e++) {
197          eye[e].pupilColor    = pupilColor;
198          eye[e].backColor     = backColor;
199          eye[e].iris.color    = irisColor;
200          eye[e].sclera.color  = scleraColor;
201          // The globally-set irisAngle and scleraAngle are read each
202          // time through because each eye has a distinct default if
203          // not set globally. Override only if set globally at first...
204          v = doc["irisAngle"];
205          if(v.is<int>())        irisAngle   = 1023 - (v.as<int>() & 1023);
206          else if(v.is<float>()) irisAngle   = 1023 - ((int)(v.as<float>() * 1024.0) & 1023);
207          else                   irisAngle   = eye[e].iris.angle;
208          eye[e].iris.angle    = eye[e].iris.startAngle   = irisAngle;
209          v = doc["scleraAngle"];
210          if(v.is<int>())        scleraAngle = 1023 - (v.as<int>() & 1023);
211          else if(v.is<float>()) scleraAngle = 1023 - ((int)(v.as<float>() * 1024.0) & 1023);
212          else                   scleraAngle = eye[e].sclera.angle;
213          eye[e].sclera.angle  = eye[e].sclera.startAngle = scleraAngle;
214          eye[e].iris.mirror   = irisMirror;
215          eye[e].sclera.mirror = scleraMirror;
216          eye[e].iris.spin     = irisSpin;
217          eye[e].sclera.spin   = scleraSpin;
218          eye[e].iris.iSpin    = irisiSpin;
219          eye[e].sclera.iSpin  = scleraiSpin;
220          // iris and sclera filenames are strdup'd for each eye rather than
221          // sharing a common pointer, reason being that it gets really messy
222          // below when overriding one or the other and trying to do the right
223          // thing with free/strdup. So this does waste a tiny bit of RAM but
224          // it's only the size of the filenames and only during init. NBD.
225          if(iristv.is<char*>())   eye[e].iris.filename   = strdup(iristv);
226          if(scleratv.is<char*>()) eye[e].sclera.filename = strdup(scleratv);
227          eye[e].rotation = rotation; // Might get override in per-eye code below
228        }
229  
230  #if NUM_EYES > 1
231        // Process any distinct per-eye settings...
232        // NOT EVERYTHING IS CONFIGURABLE PER-EYE. Color and texture stuff, yes.
233        // Other things like iris size or pupil shape are not, reason being that
234        // there isn't enough RAM for the polar angle/dist tables for two eyes.
235        for(uint8_t e=0; e<NUM_EYES; e++) {
236          eye[e].pupilColor    = dwim(doc[eye[e].name]["pupilColor"]  , eye[e].pupilColor);
237          eye[e].backColor     = dwim(doc[eye[e].name]["backColor"]   , eye[e].backColor);
238          eye[e].iris.color    = dwim(doc[eye[e].name]["irisColor"]   , eye[e].iris.color);
239          eye[e].sclera.color  = dwim(doc[eye[e].name]["scleraColor"] , eye[e].sclera.color);
240          v = doc[eye[e].name]["irisAngle"];
241          if(v.is<int>())        eye[e].iris.angle   = 1023 - (v.as<int>()   & 1023);
242          else if(v.is<float>()) eye[e].iris.angle   = 1023 - ((int)(v.as<float>()  * 1024.0) & 1023);
243          eye[e].iris.startAngle = eye[e].iris.angle;
244          v = doc[eye[e].name]["scleraAngle"];
245          if(v.is<int>())        eye[e].sclera.angle = 1023 - (v.as<int>() & 1023);
246          else if(v.is<float>()) eye[e].sclera.angle = 1023 - ((int)(v.as<float>() * 1024.0) & 1023);
247          eye[e].sclera.startAngle = eye[e].sclera.angle;
248          v = doc[eye[e].name]["irisSpin"];
249          if(v.is<float>()) eye[e].iris.spin   = v.as<float>() * -1024.0;
250          v = doc[eye[e].name]["scleraSpin"];
251          if(v.is<float>()) eye[e].sclera.spin = v.as<float>() * -1024.0;
252          v = doc[eye[e].name]["irisiSpin"];
253          if(v.is<int>()) eye[e].iris.iSpin   = v.as<int>();
254          v = doc[eye[e].name]["scleraiSpin"];
255          if(v.is<int>()) eye[e].sclera.iSpin = v.as<int>();
256          v = doc[eye[e].name]["irisMirror"];
257          if(v.is<bool>() || v.is<int>()) eye[e].iris.mirror   = v ? 1023 : 0;
258          v = doc[eye[e].name]["scleraMirror"];
259          if(v.is<bool>() || v.is<int>()) eye[e].sclera.mirror = v ? 1023 : 0;
260          v = doc[eye[e].name]["irisTexture"];
261          if(v.is<char*>()) {                           // Per-eye iris texture specified?
262            if(eye[e].iris.filename) free(eye[e].iris.filename); // Remove old name if any
263            eye[e].iris.filename = strdup(v);                    // Save new name
264          }
265          v = doc[eye[e].name]["scleraTexture"]; // Ditto w/sclera
266          if(v.is<char*>()) {
267            if(eye[e].sclera.filename) free(eye[e].sclera.filename);
268            eye[e].sclera.filename = strdup(v);
269          }
270          eye[e].rotation  = doc[eye[e].name]["rotate"] | rotation;
271          eye[e].rotation &= 3;
272        }
273  #endif
274  #if defined(ADAFRUIT_MONSTER_M4SK_EXPRESS)
275        v = doc["voice"];
276        if(v.is<bool>()) voiceOn = v.as<bool>();
277        currentPitch = defaultPitch = doc["pitch"] | defaultPitch;
278        gain = doc["gain"] | gain;
279        modulate = doc["modulate"] | modulate;
280        v = doc["waveform"];
281        if(v.is<char*>()) { // If string...
282          if(!strncasecmp(     v, "sq", 2)) waveform = 1;
283          else if(!strncasecmp(v, "si", 2)) waveform = 2;
284          else if(!strncasecmp(v, "t" , 1)) waveform = 3;
285          else if(!strncasecmp(v, "sa", 2)) waveform = 4;
286          else                              waveform = 0;
287        }
288  #endif // ADAFRUIT_MONSTER_M4SK_EXPRESS
289      }
290      file.close();
291    } else {
292      Serial.println("Can't open config file, using default settings");
293    }
294  
295    // INITIALIZE DEFAULT VALUES if config file missing or in error ----------
296  
297    // Some defaults are initialized in globals.h (because there's no way to
298    // check these for invalid input), others are initialized here if there's
299    // an obvious flag (e.g. value of 0 means "use default").
300  
301    // Default eye size is set slightly larger than the screen. This is on
302    // purpose, because displacement effect looks worst at its extremes...this
303    // allows the pupil to move close to the edge of the display while keeping
304    // a few pixels distance from the displacement limits.
305    if(!eyeRadius) eyeRadius = DISPLAY_SIZE/2 + 5;
306    else           eyeRadius = abs(eyeRadius);
307    eyeDiameter  = eyeRadius * 2;
308    eyelidIndex &= 0xFF;      // From table: learn.adafruit.com/assets/61921
309    eyelidColor  = eyelidIndex * 0x0101; // Expand eyelidIndex to 16-bit RGB
310  
311    if(!irisRadius) irisRadius = DISPLAY_SIZE/4; // Size in screen pixels
312    else            irisRadius = abs(irisRadius);
313    slitPupilRadius = abs(slitPupilRadius);
314    if(slitPupilRadius > irisRadius) slitPupilRadius = irisRadius;
315  
316    if(coverage < 0.0)      coverage = 0.0;
317    else if(coverage > 1.0) coverage = 1.0;
318    mapRadius   = (int)(eyeRadius * M_PI * coverage + 0.5);
319    mapDiameter = mapRadius * 2;
320  }
321  
322  // EYELID AND TEXTURE MAP FILE HANDLING ------------------------------------
323  
324  // Load one eyelid, convert bitmap to 2 arrays (min, max values per column).
325  // Pass in filename, destination arrays (mins, maxes, 240 elements each).
326  ImageReturnCode loadEyelid(char *filename,
327    uint8_t *minArray, uint8_t *maxArray, uint8_t init, uint32_t maxRam) {
328    Adafruit_Image  image; // Image object is on stack, pixel data is on heap
329    int32_t         w, h;
330    uint32_t        tempBytes;
331    uint8_t        *tempPtr = NULL;
332    ImageReturnCode status;
333    Adafruit_ImageReader *reader;
334  
335    yield();
336    reader = arcada.getImageReader();
337    if (!reader) {
338       return IMAGE_ERR_FILE_NOT_FOUND;
339    }
340  
341    memset(minArray, init, DISPLAY_SIZE); // Fill eyelid arrays with init value to
342    memset(maxArray, init, DISPLAY_SIZE); // mark 'no eyelid data for this column'
343  
344    // This is the "booster seat" described in m4eyes.ino
345    yield();
346    if(reader->bmpDimensions(filename, &w, &h) == IMAGE_SUCCESS) {
347      tempBytes = ((w + 7) / 8) * h; // Bitmap size in bytes
348      if (maxRam > tempBytes) {
349        if((tempPtr = (uint8_t *)malloc(maxRam - tempBytes)) != NULL) {
350          // Make SOME tempPtr reference, or optimizer removes the alloc!
351          tempPtr[0] = 0;
352        }
353      }
354      // DON'T nest the image-reading case in here. If the fragmentation
355      // culprit can be found, this block of code (and the free() block
356      // later) are easily removed.
357    }
358  
359    yield();
360    if((status = reader->loadBMP(filename, image)) == IMAGE_SUCCESS) {
361      if(image.getFormat() == IMAGE_1) { // MUST be 1-bit image
362        uint16_t *palette = image.getPalette();
363        uint8_t   white = (!palette || (palette[1] > palette[0]));
364        int       x, y, ix, iy, sx1, sx2, sy1, sy2;
365        // Center/clip eyelid image with respect to screen...
366        sx1 = (DISPLAY_SIZE - image.width()) / 2;  // leftmost pixel, screen space
367        sy1 = (DISPLAY_SIZE - image.height()) / 2; // topmost pixel, screen space
368        sx2 = sx1 + image.width() - 1;    // rightmost pixel, screen space
369        sy2 = sy1 + image.height() - 1;   // lowest pixel, screen space
370        ix  = -sx1;                       // leftmost pixel, image space
371        iy  = -sy1;                       // topmost pixel, image space
372        if(sx1 <   0) sx1 =   0;          // image wider than screen
373        if(sy1 <   0) sy1 =   0;          // image taller than screen
374        if(sx2 > (DISPLAY_SIZE-1)) sx2 = DISPLAY_SIZE - 1; // image wider than screen
375        if(sy2 > (DISPLAY_SIZE-1)) sy2 = DISPLAY_SIZE - 1; // image taller than screen
376        if(ix   <   0) ix   =   0;        // image narrower than screen
377        if(iy   <   0) iy   =   0;        // image shorter than screen
378  
379        GFXcanvas1 *canvas = (GFXcanvas1 *)image.getCanvas();
380        uint8_t    *buffer = canvas->getBuffer();
381        int         bytesPerLine = (image.width() + 7) / 8;
382        for(x=sx1; x <= sx2; x++, ix++) { // For each column...
383          yield();
384          // Get initial pointer into image buffer
385          uint8_t *ptr  = &buffer[iy * bytesPerLine + ix / 8];
386          uint8_t  mask = 0x80 >> (ix & 7); // Column mask
387          uint8_t  wbit = white ? mask : 0; // Bit value for white pixel
388          int      miny = 255, maxy;
389          for(y=sy1; y<=sy2; y++, ptr += bytesPerLine) {
390            if((*ptr & mask) == wbit) { // Is pixel set?
391              if(miny == 255) miny = y; // If 1st set pixel, set miny
392              maxy = y;                 // If set pixel at all, set max
393            }
394          }
395          if(miny != 255) {
396            // Because of coordinate system used later (screen rotated),
397            // min/max and Y coordinates are flipped before storing...
398            maxArray[x] = DISPLAY_SIZE - 1 - miny;
399            minArray[x] = DISPLAY_SIZE - 1 - maxy;
400          }
401        }
402      } else {
403        status = IMAGE_ERR_FORMAT; // Don't just return, need to dealloc...
404      }
405    }
406  
407    if(tempPtr) {
408      free(tempPtr);
409    }
410    // image destructor will handle dealloc of that object's data
411  
412    return status;
413  }
414  
415  ImageReturnCode loadTexture(char *filename, uint16_t **data,
416    uint16_t *width, uint16_t *height, uint32_t maxRam) {
417    Adafruit_Image  image; // Image object is on stack, pixel data is on heap
418    int32_t         w, h;
419    uint32_t        tempBytes;
420    uint8_t        *tempPtr = NULL;
421    ImageReturnCode status;
422    Adafruit_ImageReader *reader;
423  
424    yield();
425    reader = arcada.getImageReader();
426    if (!reader) {
427       return IMAGE_ERR_FILE_NOT_FOUND;
428    }
429    
430    // This is the "booster seat" described in m4eyes.ino
431    if(reader->bmpDimensions(filename, &w, &h) == IMAGE_SUCCESS) {
432      tempBytes = w * h * 2; // Image size in bytes (converted to 16bpp)
433      if (maxRam > tempBytes) {
434        if((tempPtr = (uint8_t *)malloc(maxRam - tempBytes)) != NULL) {
435          // Make SOME tempPtr reference, or optimizer removes the alloc!
436          tempPtr[0] = 0;
437        }
438      }
439      // DON'T nest the image-reading case in here. If the fragmentation
440      // culprit can be found, this block of code (and the free() block
441      // later) are easily removed.
442    }
443  
444    yield();
445    if((status = reader->loadBMP(filename, image)) == IMAGE_SUCCESS) {
446      if(image.getFormat() == IMAGE_16) { // MUST be 16-bit image
447        Serial.println("Texture loaded!");
448        GFXcanvas16 *canvas = (GFXcanvas16 *)image.getCanvas();
449        canvas->byteSwap(); // Match screen endianism for direct DMA xfer
450        *width  = image.width();
451        *height = image.height();
452        *data = (uint16_t *)arcada.writeDataToFlash((uint8_t *)canvas->getBuffer(),
453          (int)*width * (int)*height * 2);
454      } else {
455        status = IMAGE_ERR_FORMAT; // Don't just return, need to dealloc...
456      }
457    }
458  
459    if(tempPtr) {
460      free(tempPtr);
461    }
462    // image destructor will handle dealloc of that object's data
463  
464    return status;
465  }