-
Notifications
You must be signed in to change notification settings - Fork 0
/
file.cpp
466 lines (428 loc) · 20.2 KB
/
file.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
// SPDX-FileCopyrightText: 2019 Phillip Burgess for Adafruit Industries
//
// SPDX-License-Identifier: MIT
//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<const 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<const 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<const 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();
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"]);
gazeMax = dwim(doc["gazeMax"], gazeMax);
JsonVariant v;
v = doc["coverage"];
if(v.is<int>() || v.is<float>()) coverage = v.as<float>();
v = doc["upperEyelid"];
if(v.is<const char*>()) upperEyelidFilename = strdup(v);
v = doc["lowerEyelid"];
if(v.is<const 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<const char*>()) eye[e].iris.filename = strdup(iristv);
if(scleratv.is<const 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<const 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<const 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<const 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;
yield();
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
yield();
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...
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 = 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;
yield();
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;
}