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 }