@mentra/sdk 2.1.3 → 2.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/session/layouts.d.ts +18 -21
- package/dist/app/session/layouts.d.ts.map +1 -1
- package/dist/app/session/layouts.js +44 -55
- package/dist/app/session/modules/audio.d.ts +2 -2
- package/dist/app/session/modules/audio.d.ts.map +1 -1
- package/dist/app/session/modules/audio.js +21 -17
- package/dist/app/session/modules/camera.d.ts +4 -4
- package/dist/app/session/modules/camera.d.ts.map +1 -1
- package/dist/app/session/modules/camera.js +28 -22
- package/dist/app/session/modules/location.d.ts +3 -3
- package/dist/app/session/modules/location.d.ts.map +1 -1
- package/dist/app/session/modules/location.js +8 -5
- package/dist/types/user-session.d.ts +8 -8
- package/dist/types/user-session.d.ts.map +1 -1
- package/dist/utils/animation-utils.d.ts +4 -4
- package/dist/utils/animation-utils.d.ts.map +1 -1
- package/dist/utils/animation-utils.js +25 -24
- package/dist/utils/bitmap-utils.d.ts +4 -2
- package/dist/utils/bitmap-utils.d.ts.map +1 -1
- package/dist/utils/bitmap-utils.js +172 -45
- package/package.json +2 -1
@@ -55,6 +55,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
55
55
|
exports.BitmapUtils = void 0;
|
56
56
|
const fs = __importStar(require("fs/promises"));
|
57
57
|
const path = __importStar(require("path"));
|
58
|
+
const jimp_1 = require("jimp");
|
58
59
|
/**
|
59
60
|
* Utility class for working with bitmap images in MentraOS applications
|
60
61
|
*/
|
@@ -72,16 +73,10 @@ class BitmapUtils {
|
|
72
73
|
* session.layouts.showBitmapView(bmpHex);
|
73
74
|
* ```
|
74
75
|
*/
|
75
|
-
static async
|
76
|
+
static async loadBmpFromFileAsHex(filePath) {
|
76
77
|
try {
|
77
78
|
const bmpData = await fs.readFile(filePath);
|
78
|
-
|
79
|
-
if (bmpData.length < 14 || bmpData[0] !== 0x42 || bmpData[1] !== 0x4D) {
|
80
|
-
throw new Error(`File ${filePath} is not a valid BMP file (missing BM signature)`);
|
81
|
-
}
|
82
|
-
const hexString = bmpData.toString('hex');
|
83
|
-
console.log(`📁 Loaded BMP: ${path.basename(filePath)} (${bmpData.length} bytes)`);
|
84
|
-
return hexString;
|
79
|
+
return this.loadBmpFromDataAsHex(bmpData);
|
85
80
|
}
|
86
81
|
catch (error) {
|
87
82
|
if (error instanceof Error) {
|
@@ -90,6 +85,136 @@ class BitmapUtils {
|
|
90
85
|
throw new Error(`Failed to load BMP file ${filePath}: Unknown error`);
|
91
86
|
}
|
92
87
|
}
|
88
|
+
static async convert24BitTo1BitBMP(input24BitBmp) {
|
89
|
+
// Read header information from 24-bit BMP
|
90
|
+
const width = input24BitBmp.readUInt32LE(18);
|
91
|
+
const height = Math.abs(input24BitBmp.readInt32LE(22)); // Height can be negative (top-down BMP)
|
92
|
+
const isTopDown = input24BitBmp.readInt32LE(22) < 0;
|
93
|
+
const bitsPerPixel = input24BitBmp.readUInt16LE(28);
|
94
|
+
if (bitsPerPixel !== 24) {
|
95
|
+
throw new Error("Input must be a 24-bit BMP");
|
96
|
+
}
|
97
|
+
// Calculate row sizes (both must be 4-byte aligned)
|
98
|
+
const rowSize24 = Math.ceil((width * 3) / 4) * 4;
|
99
|
+
const rowSize1 = Math.ceil(width / 32) * 4; // 32 pixels per 4 bytes
|
100
|
+
// Calculate sizes for 1-bit BMP
|
101
|
+
const colorTableSize = 8; // 2 colors * 4 bytes each
|
102
|
+
const headerSize = 54 + colorTableSize;
|
103
|
+
const pixelDataSize = rowSize1 * height;
|
104
|
+
const fileSize = headerSize + pixelDataSize;
|
105
|
+
// Create new buffer for 1-bit BMP
|
106
|
+
const output1BitBmp = Buffer.alloc(fileSize);
|
107
|
+
let offset = 0;
|
108
|
+
// Write BMP file header (14 bytes)
|
109
|
+
output1BitBmp.write("BM", offset);
|
110
|
+
offset += 2; // Signature
|
111
|
+
output1BitBmp.writeUInt32LE(fileSize, offset);
|
112
|
+
offset += 4; // File size
|
113
|
+
output1BitBmp.writeUInt16LE(0, offset);
|
114
|
+
offset += 2; // Reserved 1
|
115
|
+
output1BitBmp.writeUInt16LE(0, offset);
|
116
|
+
offset += 2; // Reserved 2
|
117
|
+
output1BitBmp.writeUInt32LE(headerSize, offset);
|
118
|
+
offset += 4; // Pixel data offset
|
119
|
+
// Write DIB header (40 bytes)
|
120
|
+
output1BitBmp.writeUInt32LE(40, offset);
|
121
|
+
offset += 4; // DIB header size
|
122
|
+
output1BitBmp.writeInt32LE(width, offset);
|
123
|
+
offset += 4; // Width
|
124
|
+
output1BitBmp.writeInt32LE(height, offset);
|
125
|
+
offset += 4; // Height (positive for bottom-up)
|
126
|
+
output1BitBmp.writeUInt16LE(1, offset);
|
127
|
+
offset += 2; // Planes
|
128
|
+
output1BitBmp.writeUInt16LE(1, offset);
|
129
|
+
offset += 2; // Bits per pixel (1-bit)
|
130
|
+
output1BitBmp.writeUInt32LE(0, offset);
|
131
|
+
offset += 4; // Compression (none)
|
132
|
+
output1BitBmp.writeUInt32LE(pixelDataSize, offset);
|
133
|
+
offset += 4; // Image size
|
134
|
+
output1BitBmp.writeInt32LE(2835, offset);
|
135
|
+
offset += 4; // X pixels per meter (72 DPI)
|
136
|
+
output1BitBmp.writeInt32LE(2835, offset);
|
137
|
+
offset += 4; // Y pixels per meter (72 DPI)
|
138
|
+
output1BitBmp.writeUInt32LE(2, offset);
|
139
|
+
offset += 4; // Colors used
|
140
|
+
output1BitBmp.writeUInt32LE(2, offset);
|
141
|
+
offset += 4; // Important colors
|
142
|
+
// Write color table (8 bytes)
|
143
|
+
// Black (index 0): B=0, G=0, R=0, Reserved=0
|
144
|
+
output1BitBmp.writeUInt32LE(0x00000000, offset);
|
145
|
+
offset += 4;
|
146
|
+
// White (index 1): B=255, G=255, R=255, Reserved=0
|
147
|
+
output1BitBmp.writeUInt8(255, offset++); // Blue
|
148
|
+
output1BitBmp.writeUInt8(255, offset++); // Green
|
149
|
+
output1BitBmp.writeUInt8(255, offset++); // Red
|
150
|
+
output1BitBmp.writeUInt8(0, offset++); // Reserved
|
151
|
+
// Convert pixel data from 24-bit to 1-bit
|
152
|
+
const pixelDataStart24 = 54; // 24-bit BMP has no color table
|
153
|
+
for (let y = 0; y < height; y++) {
|
154
|
+
// BMP files are usually stored bottom-up
|
155
|
+
const sourceY = isTopDown ? y : height - 1 - y;
|
156
|
+
const destY = height - 1 - y; // Always write bottom-up for compatibility
|
157
|
+
// Initialize the row with zeros
|
158
|
+
const rowData = Buffer.alloc(rowSize1);
|
159
|
+
for (let x = 0; x < width; x++) {
|
160
|
+
// Get pixel from 24-bit BMP
|
161
|
+
const offset24 = pixelDataStart24 + sourceY * rowSize24 + x * 3;
|
162
|
+
const blue = input24BitBmp[offset24];
|
163
|
+
const green = input24BitBmp[offset24 + 1];
|
164
|
+
const red = input24BitBmp[offset24 + 2];
|
165
|
+
// Determine if pixel is white (assuming pure black or white)
|
166
|
+
// White = 1, Black = 0
|
167
|
+
const isWhite = red > 128 || green > 128 || blue > 128 ? 1 : 0;
|
168
|
+
// Calculate bit position
|
169
|
+
const byteIndex = Math.floor(x / 8);
|
170
|
+
const bitPosition = 7 - (x % 8); // MSB first
|
171
|
+
// Set bit if white
|
172
|
+
if (isWhite) {
|
173
|
+
rowData[byteIndex] |= 1 << bitPosition;
|
174
|
+
}
|
175
|
+
}
|
176
|
+
// Write row to output buffer
|
177
|
+
const destOffset = offset + destY * rowSize1;
|
178
|
+
rowData.copy(output1BitBmp, destOffset);
|
179
|
+
}
|
180
|
+
return output1BitBmp;
|
181
|
+
}
|
182
|
+
static async loadBmpFromDataAsHex(bmpData) {
|
183
|
+
try {
|
184
|
+
// Basic BMP validation - check for BMP signature
|
185
|
+
if (bmpData.length < 14 || bmpData[0] !== 0x42 || bmpData[1] !== 0x4d) {
|
186
|
+
throw new Error(`Bmp data is not a valid BMP file (missing BM signature)`);
|
187
|
+
}
|
188
|
+
let finalBmpData = bmpData;
|
189
|
+
// Load the image with Jimp
|
190
|
+
const image = await jimp_1.Jimp.read(bmpData);
|
191
|
+
// Check if we need to add padding
|
192
|
+
if (image.width !== 576 || image.height !== 135) {
|
193
|
+
console.log(`Adding padding to BMP since it isn't 576x135 (current: ${image.width}x${image.height})`);
|
194
|
+
// Create a new 576x135 white canvas
|
195
|
+
const paddedImage = new jimp_1.Jimp({
|
196
|
+
width: 576,
|
197
|
+
height: 135,
|
198
|
+
color: 0xffffffff,
|
199
|
+
});
|
200
|
+
// // Calculate position to place the original image (with padding)
|
201
|
+
const leftPadding = 40; // 40px padding on left
|
202
|
+
const topPadding = 35; // 35px padding on top
|
203
|
+
// Composite the original image onto the white canvas
|
204
|
+
paddedImage.composite(image, leftPadding, topPadding);
|
205
|
+
finalBmpData = await this.convert24BitTo1BitBMP(await paddedImage.getBuffer("image/bmp"));
|
206
|
+
}
|
207
|
+
// No padding needed, just return as hex
|
208
|
+
console.log(`finalBmpData: ${finalBmpData.length} bytes`);
|
209
|
+
return finalBmpData.toString("hex");
|
210
|
+
}
|
211
|
+
catch (error) {
|
212
|
+
if (error instanceof Error) {
|
213
|
+
throw new Error(`Failed to load BMP data: ${error.message}`);
|
214
|
+
}
|
215
|
+
throw new Error(`Failed to load BMP data: Unknown error`);
|
216
|
+
}
|
217
|
+
}
|
93
218
|
/**
|
94
219
|
* Load multiple BMP frames as hex array for animations
|
95
220
|
*
|
@@ -111,19 +236,19 @@ class BitmapUtils {
|
|
111
236
|
* ```
|
112
237
|
*/
|
113
238
|
static async loadBmpFrames(basePath, frameCount, options = {}) {
|
114
|
-
const { filePattern =
|
239
|
+
const { filePattern = "animation_10_frame_{i}.bmp", startFrame = 1, validateFrames = true, skipMissingFrames = false, } = options;
|
115
240
|
const frames = [];
|
116
241
|
const errors = [];
|
117
242
|
for (let i = 0; i < frameCount; i++) {
|
118
243
|
const frameNumber = startFrame + i;
|
119
|
-
const fileName = filePattern.replace(
|
244
|
+
const fileName = filePattern.replace("{i}", frameNumber.toString());
|
120
245
|
const filePath = path.join(basePath, fileName);
|
121
246
|
try {
|
122
|
-
const frameHex = await this.
|
247
|
+
const frameHex = await this.loadBmpFromFileAsHex(filePath);
|
123
248
|
if (validateFrames) {
|
124
249
|
const validation = this.validateBmpHex(frameHex);
|
125
250
|
if (!validation.isValid) {
|
126
|
-
const errorMsg = `Frame ${frameNumber} validation failed: ${validation.errors.join(
|
251
|
+
const errorMsg = `Frame ${frameNumber} validation failed: ${validation.errors.join(", ")}`;
|
127
252
|
if (skipMissingFrames) {
|
128
253
|
console.warn(`⚠️ ${errorMsg} - skipping`);
|
129
254
|
continue;
|
@@ -137,7 +262,7 @@ class BitmapUtils {
|
|
137
262
|
frames.push(frameHex);
|
138
263
|
}
|
139
264
|
catch (error) {
|
140
|
-
const errorMsg = `Failed to load frame ${frameNumber} (${fileName}): ${error instanceof Error ? error.message :
|
265
|
+
const errorMsg = `Failed to load frame ${frameNumber} (${fileName}): ${error instanceof Error ? error.message : "Unknown error"}`;
|
141
266
|
if (skipMissingFrames) {
|
142
267
|
console.warn(`⚠️ ${errorMsg} - skipping`);
|
143
268
|
continue;
|
@@ -148,7 +273,7 @@ class BitmapUtils {
|
|
148
273
|
}
|
149
274
|
}
|
150
275
|
if (errors.length > 0) {
|
151
|
-
throw new Error(`Failed to load frames:\n${errors.join(
|
276
|
+
throw new Error(`Failed to load frames:\n${errors.join("\n")}`);
|
152
277
|
}
|
153
278
|
if (frames.length === 0) {
|
154
279
|
throw new Error(`No valid frames loaded from ${basePath}`);
|
@@ -176,35 +301,37 @@ class BitmapUtils {
|
|
176
301
|
const errors = [];
|
177
302
|
let byteCount = 0;
|
178
303
|
let blackPixels = 0;
|
179
|
-
|
304
|
+
const metadata = {};
|
180
305
|
try {
|
181
306
|
// Basic hex validation
|
182
|
-
if (typeof hexString !==
|
183
|
-
errors.push(
|
307
|
+
if (typeof hexString !== "string" || hexString.length === 0) {
|
308
|
+
errors.push("Hex string is empty or invalid");
|
184
309
|
return { isValid: false, byteCount: 0, blackPixels: 0, errors };
|
185
310
|
}
|
186
311
|
if (hexString.length % 2 !== 0) {
|
187
|
-
errors.push(
|
312
|
+
errors.push("Hex string length must be even");
|
188
313
|
return { isValid: false, byteCount: 0, blackPixels: 0, errors };
|
189
314
|
}
|
190
315
|
// Convert to buffer
|
191
|
-
const buffer = Buffer.from(hexString,
|
316
|
+
const buffer = Buffer.from(hexString, "hex");
|
192
317
|
byteCount = buffer.length;
|
193
318
|
// BMP signature validation
|
194
319
|
if (buffer.length < 14) {
|
195
|
-
errors.push(
|
320
|
+
errors.push("File too small to be a valid BMP (minimum 14 bytes for header)");
|
196
321
|
}
|
197
322
|
else {
|
198
|
-
if (buffer[0] !== 0x42 || buffer[1] !==
|
323
|
+
if (buffer[0] !== 0x42 || buffer[1] !== 0x4d) {
|
199
324
|
errors.push('Invalid BMP signature (should start with "BM")');
|
200
325
|
}
|
201
326
|
}
|
202
327
|
// Size validation for MentraOS (576x135 = ~9782 bytes expected)
|
203
328
|
const expectedSize = 9782;
|
204
|
-
if (buffer.length < expectedSize - 100) {
|
329
|
+
if (buffer.length < expectedSize - 100) {
|
330
|
+
// Allow some tolerance
|
205
331
|
errors.push(`BMP too small (${buffer.length} bytes, expected ~${expectedSize})`);
|
206
332
|
}
|
207
|
-
else if (buffer.length > expectedSize + 1000) {
|
333
|
+
else if (buffer.length > expectedSize + 1000) {
|
334
|
+
// Allow some tolerance
|
208
335
|
errors.push(`BMP too large (${buffer.length} bytes, expected ~${expectedSize})`);
|
209
336
|
}
|
210
337
|
// Extract BMP metadata if header is valid
|
@@ -214,37 +341,37 @@ class BitmapUtils {
|
|
214
341
|
const width = buffer.readUInt32LE(18);
|
215
342
|
const height = buffer.readUInt32LE(22);
|
216
343
|
metadata.dimensions = { width, height };
|
217
|
-
metadata.format =
|
344
|
+
metadata.format = "BMP";
|
218
345
|
// Validate dimensions for MentraOS glasses
|
219
346
|
if (width !== 576 || height !== 135) {
|
220
347
|
errors.push(`Invalid dimensions (${width}x${height}, expected 576x135 for MentraOS)`);
|
221
348
|
}
|
222
349
|
}
|
223
350
|
catch (e) {
|
224
|
-
errors.push(
|
351
|
+
errors.push("Failed to parse BMP header metadata");
|
225
352
|
}
|
226
353
|
}
|
227
354
|
// Pixel data validation (assumes 54-byte header + pixel data)
|
228
355
|
if (buffer.length > 62) {
|
229
356
|
const pixelData = buffer.slice(62); // Skip BMP header
|
230
|
-
blackPixels = Array.from(pixelData).filter(b => b !==
|
357
|
+
blackPixels = Array.from(pixelData).filter((b) => b !== 0xff).length;
|
231
358
|
if (blackPixels === 0) {
|
232
|
-
errors.push(
|
359
|
+
errors.push("No black pixels found (image appears to be all white)");
|
233
360
|
}
|
234
361
|
}
|
235
362
|
else {
|
236
|
-
errors.push(
|
363
|
+
errors.push("File too small to contain pixel data");
|
237
364
|
}
|
238
365
|
}
|
239
366
|
catch (error) {
|
240
|
-
errors.push(`Failed to parse hex data: ${error instanceof Error ? error.message :
|
367
|
+
errors.push(`Failed to parse hex data: ${error instanceof Error ? error.message : "Unknown error"}`);
|
241
368
|
}
|
242
369
|
return {
|
243
370
|
isValid: errors.length === 0,
|
244
371
|
byteCount,
|
245
372
|
blackPixels,
|
246
373
|
errors,
|
247
|
-
metadata: Object.keys(metadata).length > 0 ? metadata : undefined
|
374
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
248
375
|
};
|
249
376
|
}
|
250
377
|
/**
|
@@ -265,13 +392,13 @@ class BitmapUtils {
|
|
265
392
|
let buffer;
|
266
393
|
// Convert input to buffer
|
267
394
|
switch (fromFormat) {
|
268
|
-
case
|
269
|
-
buffer = Buffer.from(data,
|
395
|
+
case "hex":
|
396
|
+
buffer = Buffer.from(data, "hex");
|
270
397
|
break;
|
271
|
-
case
|
272
|
-
buffer = Buffer.from(data,
|
398
|
+
case "base64":
|
399
|
+
buffer = Buffer.from(data, "base64");
|
273
400
|
break;
|
274
|
-
case
|
401
|
+
case "buffer":
|
275
402
|
buffer = data;
|
276
403
|
break;
|
277
404
|
default:
|
@@ -279,11 +406,11 @@ class BitmapUtils {
|
|
279
406
|
}
|
280
407
|
// Convert buffer to target format
|
281
408
|
switch (toFormat) {
|
282
|
-
case
|
283
|
-
return buffer.toString(
|
284
|
-
case
|
285
|
-
return buffer.toString(
|
286
|
-
case
|
409
|
+
case "hex":
|
410
|
+
return buffer.toString("hex");
|
411
|
+
case "base64":
|
412
|
+
return buffer.toString("base64");
|
413
|
+
case "buffer":
|
287
414
|
return buffer;
|
288
415
|
default:
|
289
416
|
throw new Error(`Unsupported target format: ${toFormat}`);
|
@@ -303,8 +430,8 @@ class BitmapUtils {
|
|
303
430
|
*/
|
304
431
|
static getBitmapInfo(hexString) {
|
305
432
|
try {
|
306
|
-
const buffer = Buffer.from(hexString,
|
307
|
-
const isValidBmp = buffer.length >= 14 && buffer[0] === 0x42 && buffer[1] ===
|
433
|
+
const buffer = Buffer.from(hexString, "hex");
|
434
|
+
const isValidBmp = buffer.length >= 14 && buffer[0] === 0x42 && buffer[1] === 0x4d;
|
308
435
|
let width;
|
309
436
|
let height;
|
310
437
|
if (isValidBmp && buffer.length >= 54) {
|
@@ -317,20 +444,20 @@ class BitmapUtils {
|
|
317
444
|
}
|
318
445
|
}
|
319
446
|
const pixelData = buffer.slice(62);
|
320
|
-
const blackPixels = Array.from(pixelData).filter(b => b !==
|
447
|
+
const blackPixels = Array.from(pixelData).filter((b) => b !== 0xff).length;
|
321
448
|
return {
|
322
449
|
byteCount: buffer.length,
|
323
450
|
blackPixels,
|
324
451
|
width,
|
325
452
|
height,
|
326
|
-
isValidBmp
|
453
|
+
isValidBmp,
|
327
454
|
};
|
328
455
|
}
|
329
456
|
catch (error) {
|
330
457
|
return {
|
331
458
|
byteCount: 0,
|
332
459
|
blackPixels: 0,
|
333
|
-
isValidBmp: false
|
460
|
+
isValidBmp: false,
|
334
461
|
};
|
335
462
|
}
|
336
463
|
}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@mentra/sdk",
|
3
|
-
"version": "2.1.
|
3
|
+
"version": "2.1.4",
|
4
4
|
"description": "Build apps for MentraOS smartglasses. This SDK provides everything you need to create real-time smartglasses applications.",
|
5
5
|
"source": "src/index.ts",
|
6
6
|
"main": "dist/index.js",
|
@@ -29,6 +29,7 @@
|
|
29
29
|
"cookie-parser": "^1.4.7",
|
30
30
|
"dotenv": "^16.4.0",
|
31
31
|
"express": "^4.18.2",
|
32
|
+
"jimp": "^1.6.0",
|
32
33
|
"jsonwebtoken": "^8.5.1",
|
33
34
|
"jsrsasign": "^11.1.0",
|
34
35
|
"multer": "^2.0.1",
|