@mentra/sdk 2.1.3 → 2.1.5
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/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
@@ -15,7 +15,7 @@
|
|
15
15
|
* layouts.showReferenceCard('Weather', 'Sunny and 75°F');
|
16
16
|
* ```
|
17
17
|
*/
|
18
|
-
import { DisplayRequest, ViewType } from
|
18
|
+
import { DisplayRequest, ViewType } from "../../types";
|
19
19
|
export declare class LayoutManager {
|
20
20
|
private packageName;
|
21
21
|
private sendMessage;
|
@@ -109,28 +109,25 @@ export declare class LayoutManager {
|
|
109
109
|
durationMs?: number;
|
110
110
|
}): void;
|
111
111
|
/**
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
112
|
+
* 📇 Shows a bitmap
|
113
|
+
*
|
114
|
+
* Uses the proven animation system internally for proper L/R eye synchronization.
|
115
|
+
* This ensures single bitmap displays work consistently with the same
|
116
|
+
* hardware-optimized timing as animations.
|
117
|
+
*
|
118
|
+
* @param data - hex or base64 encoded bitmap data
|
119
|
+
* @param options - Optional parameters (view, duration)
|
120
|
+
*
|
121
|
+
* @example
|
122
|
+
* ```typescript
|
123
|
+
* layouts.showBitmapView(
|
124
|
+
* yourHexOrBase64EncodedBitmapDataString
|
125
|
+
* );
|
126
|
+
* ```
|
127
|
+
*/
|
128
128
|
showBitmapView(data: string, options?: {
|
129
129
|
view?: ViewType;
|
130
|
-
|
131
|
-
}): {
|
132
|
-
stop: () => void;
|
133
|
-
};
|
130
|
+
}): void;
|
134
131
|
/**
|
135
132
|
* 📊 Shows a dashboard card with left and right text
|
136
133
|
*
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"layouts.d.ts","sourceRoot":"","sources":["../../../src/app/session/layouts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EACL,cAAc,EAOd,QAAQ,EAKT,MAAM,aAAa,CAAC;AAErB,qBAAa,aAAa;IAQtB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,WAAW;IARrB;;;;;OAKG;gBAEO,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI;IAGxD;;;;;;;;OAQG;IACH,OAAO,CAAC,kBAAkB;
|
1
|
+
{"version":3,"file":"layouts.d.ts","sourceRoot":"","sources":["../../../src/app/session/layouts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EACL,cAAc,EAOd,QAAQ,EAKT,MAAM,aAAa,CAAC;AAErB,qBAAa,aAAa;IAQtB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,WAAW;IARrB;;;;;OAKG;gBAEO,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI;IAGxD;;;;;;;;OAQG;IACH,OAAO,CAAC,kBAAkB;IAoH1B;;;;;;;;;;;;;;;;;OAiBG;IACH,YAAY,CACV,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE;IAuCpD;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,kBAAkB,CAChB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE;IAYpD;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,iBAAiB,CACf,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE;IAYpD;;;;;;;;;;;;;;;;OAgBG;IACH,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,QAAQ,CAAA;KAAE;IAQ1D;;;;;;;;;;;;;;;;OAgBG;IACH,iBAAiB,CACf,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE;IAgBpD;;;;;;;;;;;;;;;OAeG;IACH,SAAS,CAAC,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,QAAQ,CAAA;KAAE;IAOvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,mBAAmB,CACjB,eAAe,EAAE,MAAM,EAAE,EACzB,UAAU,GAAE,MAAa,EACzB,MAAM,GAAE,OAAe,EACvB,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,QAAQ,CAAA;KAAE,GAC5B;QAAE,IAAI,EAAE,MAAM,IAAI,CAAA;KAAE;CAiCxB"}
|
@@ -51,7 +51,7 @@ class LayoutManager {
|
|
51
51
|
// Layout-specific validations
|
52
52
|
switch (layout.layoutType) {
|
53
53
|
case types_1.LayoutType.TEXT_WALL:
|
54
|
-
if (typeof layout.text !==
|
54
|
+
if (typeof layout.text !== "string") {
|
55
55
|
throw new Error("TextWall layout must have a text property");
|
56
56
|
}
|
57
57
|
// Ensure text is not too long (prevent performance issues)
|
@@ -61,38 +61,39 @@ class LayoutManager {
|
|
61
61
|
break;
|
62
62
|
case types_1.LayoutType.DOUBLE_TEXT_WALL:
|
63
63
|
const doubleText = layout;
|
64
|
-
if (typeof doubleText.topText !==
|
64
|
+
if (typeof doubleText.topText !== "string") {
|
65
65
|
throw new Error("DoubleTextWall layout must have a topText property");
|
66
66
|
}
|
67
|
-
if (typeof doubleText.bottomText !==
|
67
|
+
if (typeof doubleText.bottomText !== "string") {
|
68
68
|
throw new Error("DoubleTextWall layout must have a bottomText property");
|
69
69
|
}
|
70
70
|
break;
|
71
71
|
case types_1.LayoutType.REFERENCE_CARD:
|
72
72
|
const refCard = layout;
|
73
|
-
if (typeof refCard.title !==
|
73
|
+
if (typeof refCard.title !== "string") {
|
74
74
|
throw new Error("ReferenceCard layout must have a title property");
|
75
75
|
}
|
76
|
-
if (typeof refCard.text !==
|
76
|
+
if (typeof refCard.text !== "string") {
|
77
77
|
throw new Error("ReferenceCard layout must have a text property");
|
78
78
|
}
|
79
79
|
break;
|
80
80
|
case types_1.LayoutType.DASHBOARD_CARD:
|
81
81
|
const dashCard = layout;
|
82
|
-
if (typeof dashCard.leftText !==
|
82
|
+
if (typeof dashCard.leftText !== "string") {
|
83
83
|
throw new Error("DashboardCard layout must have a leftText property");
|
84
84
|
}
|
85
|
-
if (typeof dashCard.rightText !==
|
85
|
+
if (typeof dashCard.rightText !== "string") {
|
86
86
|
throw new Error("DashboardCard layout must have a rightText property");
|
87
87
|
}
|
88
88
|
break;
|
89
89
|
case types_1.LayoutType.BITMAP_VIEW:
|
90
90
|
const bitmapView = layout;
|
91
|
-
if (typeof bitmapView.data !==
|
91
|
+
if (typeof bitmapView.data !== "string") {
|
92
92
|
throw new Error("BitmapView layout must have a data property");
|
93
93
|
}
|
94
94
|
// Check if data is too large (prevent OOM errors)
|
95
|
-
if (bitmapView.data.length > 1000000) {
|
95
|
+
if (bitmapView.data.length > 1000000) {
|
96
|
+
// 1MB limit
|
96
97
|
throw new Error("Bitmap data is too large (>1MB), please reduce size");
|
97
98
|
}
|
98
99
|
break;
|
@@ -107,7 +108,7 @@ class LayoutManager {
|
|
107
108
|
}
|
108
109
|
// Validate duration if provided
|
109
110
|
if (durationMs !== undefined) {
|
110
|
-
if (typeof durationMs !==
|
111
|
+
if (typeof durationMs !== "number" || durationMs < 0) {
|
111
112
|
console.warn(`Invalid duration: ${durationMs}, ignoring`);
|
112
113
|
durationMs = undefined;
|
113
114
|
}
|
@@ -115,12 +116,12 @@ class LayoutManager {
|
|
115
116
|
// Create the display request with validated data
|
116
117
|
return {
|
117
118
|
timestamp: new Date(),
|
118
|
-
sessionId:
|
119
|
+
sessionId: "", // Will be filled by session
|
119
120
|
type: types_1.AppToCloudMessageType.DISPLAY_REQUEST,
|
120
121
|
packageName: this.packageName,
|
121
122
|
view,
|
122
123
|
layout,
|
123
|
-
durationMs
|
124
|
+
durationMs,
|
124
125
|
};
|
125
126
|
}
|
126
127
|
catch (error) {
|
@@ -154,14 +155,14 @@ class LayoutManager {
|
|
154
155
|
console.warn("showTextWall called with null/undefined text");
|
155
156
|
}
|
156
157
|
// Ensure text is a string
|
157
|
-
if (typeof text !==
|
158
|
+
if (typeof text !== "string") {
|
158
159
|
text = String(text); // Convert to string
|
159
160
|
console.warn("showTextWall: Non-string input converted to string");
|
160
161
|
}
|
161
162
|
// Create layout with validated text
|
162
163
|
const layout = {
|
163
164
|
layoutType: types_1.LayoutType.TEXT_WALL,
|
164
|
-
text
|
165
|
+
text,
|
165
166
|
};
|
166
167
|
// Create and send display event with error handling
|
167
168
|
try {
|
@@ -203,7 +204,7 @@ class LayoutManager {
|
|
203
204
|
const layout = {
|
204
205
|
layoutType: types_1.LayoutType.DOUBLE_TEXT_WALL,
|
205
206
|
topText,
|
206
|
-
bottomText
|
207
|
+
bottomText,
|
207
208
|
};
|
208
209
|
this.sendMessage(this.createDisplayEvent(layout, options?.view, options?.durationMs));
|
209
210
|
}
|
@@ -232,45 +233,33 @@ class LayoutManager {
|
|
232
233
|
const layout = {
|
233
234
|
layoutType: types_1.LayoutType.REFERENCE_CARD,
|
234
235
|
title,
|
235
|
-
text
|
236
|
+
text,
|
236
237
|
};
|
237
238
|
this.sendMessage(this.createDisplayEvent(layout, options?.view, options?.durationMs));
|
238
239
|
}
|
239
240
|
/**
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
241
|
+
* 📇 Shows a bitmap
|
242
|
+
*
|
243
|
+
* Uses the proven animation system internally for proper L/R eye synchronization.
|
244
|
+
* This ensures single bitmap displays work consistently with the same
|
245
|
+
* hardware-optimized timing as animations.
|
246
|
+
*
|
247
|
+
* @param data - hex or base64 encoded bitmap data
|
248
|
+
* @param options - Optional parameters (view, duration)
|
249
|
+
*
|
250
|
+
* @example
|
251
|
+
* ```typescript
|
252
|
+
* layouts.showBitmapView(
|
253
|
+
* yourHexOrBase64EncodedBitmapDataString
|
254
|
+
* );
|
255
|
+
* ```
|
256
|
+
*/
|
256
257
|
showBitmapView(data, options) {
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
// Create short multi-frame animation for better L/R sync
|
263
|
-
// Repeat the same frame a few times to mimic continuous animation behavior
|
264
|
-
const frames = [data, data, data]; // Triple the frame for stability
|
265
|
-
const animation = this.showBitmapAnimation(frames, // Multi-frame array (same image repeated)
|
266
|
-
frameInterval, // Use proven 1650ms interval
|
267
|
-
false, // Don't repeat infinitely
|
268
|
-
{ view: options?.view });
|
269
|
-
// Auto-stop after duration
|
270
|
-
setTimeout(() => {
|
271
|
-
animation.stop();
|
272
|
-
}, duration);
|
273
|
-
return animation;
|
258
|
+
const layout = {
|
259
|
+
layoutType: types_1.LayoutType.BITMAP_VIEW,
|
260
|
+
data: data,
|
261
|
+
};
|
262
|
+
this.sendMessage(this.createDisplayEvent(layout, options?.view));
|
274
263
|
}
|
275
264
|
/**
|
276
265
|
* 📊 Shows a dashboard card with left and right text
|
@@ -293,7 +282,7 @@ class LayoutManager {
|
|
293
282
|
const layout = {
|
294
283
|
layoutType: types_1.LayoutType.DASHBOARD_CARD,
|
295
284
|
leftText,
|
296
|
-
rightText
|
285
|
+
rightText,
|
297
286
|
};
|
298
287
|
this.sendMessage(this.createDisplayEvent(layout, options?.view || types_1.ViewType.DASHBOARD, options?.durationMs));
|
299
288
|
}
|
@@ -315,7 +304,7 @@ class LayoutManager {
|
|
315
304
|
*/
|
316
305
|
clearView(options) {
|
317
306
|
const layout = {
|
318
|
-
layoutType: types_1.LayoutType.CLEAR_VIEW
|
307
|
+
layoutType: types_1.LayoutType.CLEAR_VIEW,
|
319
308
|
};
|
320
309
|
this.sendMessage(this.createDisplayEvent(layout, options?.view));
|
321
310
|
}
|
@@ -359,17 +348,17 @@ class LayoutManager {
|
|
359
348
|
layoutType: types_1.LayoutType.BITMAP_ANIMATION,
|
360
349
|
frames: bitmapDataArray,
|
361
350
|
interval: intervalMs,
|
362
|
-
repeat: repeat
|
351
|
+
repeat: repeat,
|
363
352
|
};
|
364
353
|
this.sendMessage(this.createDisplayEvent(layout, options?.view));
|
365
|
-
console.log(`🎬 Sent batched animation to iOS: ${bitmapDataArray.length} frames at ${intervalMs}ms${repeat ?
|
354
|
+
console.log(`🎬 Sent batched animation to iOS: ${bitmapDataArray.length} frames at ${intervalMs}ms${repeat ? " (repeating)" : ""}`);
|
366
355
|
// Return controller for compatibility
|
367
356
|
return {
|
368
357
|
stop: () => {
|
369
358
|
// Send stop command to iOS
|
370
359
|
this.clearView();
|
371
|
-
console.log(
|
372
|
-
}
|
360
|
+
console.log("🛑 Animation stop requested");
|
361
|
+
},
|
373
362
|
};
|
374
363
|
}
|
375
364
|
}
|
@@ -1,10 +1,10 @@
|
|
1
|
-
import { WebSocket } from
|
2
|
-
import { AppI, TranscriptI } from
|
3
|
-
import { DisplayRequest } from
|
4
|
-
import { Transform } from
|
5
|
-
import { ConversationTranscriber, PushAudioInputStream } from
|
6
|
-
import { ExtendedStreamType } from
|
7
|
-
import pino from
|
1
|
+
import { WebSocket } from "ws";
|
2
|
+
import { AppI, TranscriptI } from "./models";
|
3
|
+
import { DisplayRequest } from "./layouts";
|
4
|
+
import { Transform } from "stream";
|
5
|
+
import { ConversationTranscriber, PushAudioInputStream } from "microsoft-cognitiveservices-speech-sdk";
|
6
|
+
import { ExtendedStreamType } from "./streams";
|
7
|
+
import pino from "pino";
|
8
8
|
/**
|
9
9
|
* Session for an application
|
10
10
|
*/
|
@@ -53,7 +53,7 @@ export interface UserSession {
|
|
53
53
|
installedApps: AppI[];
|
54
54
|
activeAppSessions: string[];
|
55
55
|
loadingApps: Set<string>;
|
56
|
-
appSubscriptions: Map<string, ExtendedStreamType[]> |
|
56
|
+
appSubscriptions: Map<string, ExtendedStreamType[]> | object;
|
57
57
|
appConnections: Map<string, WebSocket>;
|
58
58
|
websocket: WebSocket;
|
59
59
|
transcript: TranscriptI;
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"user-session.d.ts","sourceRoot":"","sources":["../../src/types/user-session.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,EAAE,IAAI,EAAe,WAAW,EAAE,MAAM,UAAU,CAAC;AAE1D,OAAO,EAAE,cAAc,EAAU,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,EACL,uBAAuB,EACvB,oBAAoB,EACrB,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAc,MAAM,WAAW,CAAC;AAE3D,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB;;GAEG;AAkBH;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,eAAgB,SAAQ,SAAS;CAAG;AAErD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,kBAAkB,
|
1
|
+
{"version":3,"file":"user-session.d.ts","sourceRoot":"","sources":["../../src/types/user-session.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,EAAE,IAAI,EAAe,WAAW,EAAE,MAAM,UAAU,CAAC;AAE1D,OAAO,EAAE,cAAc,EAAU,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,EACL,uBAAuB,EACvB,oBAAoB,EACrB,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAc,MAAM,WAAW,CAAC;AAE3D,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB;;GAEG;AAkBH;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,eAAgB,SAAQ,SAAS;CAAG;AAErD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,kBAAkB,CAChB,cAAc,EAAE,cAAc,EAC9B,WAAW,EAAE,WAAW,GACvB,OAAO,CAAC;IACX,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IACpE,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;CACpE;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,cAAc,EAAE,cAAc,CAAC;IAC/B,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,CAAC,EAAE,IAAI,CAAC;CAClB;AAED;;GAEG;AAEH,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,IAAI,CAAC;IAChB,cAAc,EAAE,IAAI,GAAG,IAAI,CAAC;IAG5B,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;IAGpB,aAAa,EAAE,IAAI,EAAE,CAAC;IACtB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,EAAE,CAAC,GAAG,MAAM,CAAC;IAC7D,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAEvC,SAAS,EAAE,SAAS,CAAC;IACrB,UAAU,EAAE,WAAW,CAAC;IAGxB,UAAU,CAAC,EAAE,oBAAoB,CAAC;IAClC,UAAU,CAAC,EAAE,uBAAuB,CAAC;IACrC,cAAc,EAAE,OAAO,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAG9B,aAAa,EAAE,eAAe,EAAE,CAAC;IAGjC,cAAc,CAAC,EAAE,eAAe,CAAC;IACjC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAG5B,YAAY,EAAE,kBAAkB,EAAE,CAAC;CACpC;AAED;;GAEG"}
|
@@ -20,8 +20,8 @@
|
|
20
20
|
* animation.stop();
|
21
21
|
* ```
|
22
22
|
*/
|
23
|
-
import { AppSession } from
|
24
|
-
import { LoadFramesOptions } from
|
23
|
+
import { AppSession } from "../app/session";
|
24
|
+
import { LoadFramesOptions } from "./bitmap-utils";
|
25
25
|
/**
|
26
26
|
* Configuration options for bitmap animations
|
27
27
|
*/
|
@@ -132,7 +132,7 @@ export declare class AnimationUtils {
|
|
132
132
|
* );
|
133
133
|
* ```
|
134
134
|
*/
|
135
|
-
static createBitmapAnimationFromFrames(session: AppSession, frames: string[], config?: Omit<AnimationConfig,
|
135
|
+
static createBitmapAnimationFromFrames(session: AppSession, frames: string[], config?: Omit<AnimationConfig, "loadOptions" | "validateFrames">): AnimationController;
|
136
136
|
/**
|
137
137
|
* Create a sequence of bitmap displays with custom timing
|
138
138
|
*
|
@@ -182,7 +182,7 @@ export declare class AnimationUtils {
|
|
182
182
|
* );
|
183
183
|
* ```
|
184
184
|
*/
|
185
|
-
static getOptimizedConfig(deviceType:
|
185
|
+
static getOptimizedConfig(deviceType: "even-realities-g1" | "generic"): AnimationConfig;
|
186
186
|
/**
|
187
187
|
* Preload and cache animation frames for better performance
|
188
188
|
*
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"animation-utils.d.ts","sourceRoot":"","sources":["../../src/utils/animation-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAe,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAEhE;;GAEG;AACH,MAAM,WAAW,eAAe;
|
1
|
+
{"version":3,"file":"animation-utils.d.ts","sourceRoot":"","sources":["../../src/utils/animation-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAe,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAEhE;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,qFAAqF;IACrF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,gEAAgE;IAChE,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,4CAA4C;IAC5C,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5D,sDAAsD;IACtD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,yBAAyB;IACzB,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,8CAA8C;IAC9C,SAAS,EAAE,MAAM,OAAO,CAAC;IACzB,8BAA8B;IAC9B,eAAe,EAAE,MAAM,MAAM,CAAC;IAC9B,4BAA4B;IAC5B,cAAc,EAAE,MAAM,MAAM,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,qCAAqC;IACrC,cAAc,EAAE,MAAM,CAAC;IACvB,8CAA8C;IAC9C,cAAc,EAAE,MAAM,CAAC;IACvB,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,qCAAqC;IACrC,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;GAEG;AACH,qBAAa,cAAc;IACzB;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvC;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;WACU,qBAAqB,CAChC,OAAO,EAAE,UAAU,EACnB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,eAAoB,GAC3B,OAAO,CAAC,mBAAmB,CAAC;IAkD/B;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,+BAA+B,CACpC,OAAO,EAAE,UAAU,EACnB,MAAM,EAAE,MAAM,EAAE,EAChB,MAAM,GAAE,IAAI,CAAC,eAAe,EAAE,aAAa,GAAG,gBAAgB,CAAM,GACnE,mBAAmB;IA6FtB;;;;;;;;;;;;;;;OAeG;WACU,oBAAoB,CAC/B,OAAO,EAAE,UAAU,EACnB,QAAQ,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,GACnD,OAAO,CAAC,IAAI,CAAC;IA2BhB;;;;;;;;;;;;;OAaG;WACU,aAAa,CACxB,cAAc,EAAE,MAAM,EACtB,eAAe,GAAE,MAAc,GAC9B,OAAO,CAAC,UAAU,CAAC;IA2CtB;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,kBAAkB,CACvB,UAAU,EAAE,mBAAmB,GAAG,SAAS,GAC1C,eAAe;IA2BlB;;;;;;;;;;;;;;;;;OAiBG;WACU,aAAa,CACxB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,MAAM,EAAE,CAAC;CAcrB"}
|
@@ -42,7 +42,7 @@ class AnimationUtils {
|
|
42
42
|
* ```
|
43
43
|
*/
|
44
44
|
static delay(ms) {
|
45
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
45
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
46
46
|
}
|
47
47
|
/**
|
48
48
|
* Create bitmap animation from files with advanced configuration
|
@@ -74,16 +74,16 @@ class AnimationUtils {
|
|
74
74
|
*/
|
75
75
|
static async createBitmapAnimation(session, basePath, frameCount, config = {}) {
|
76
76
|
const { intervalMs = 1750, // Optimized for MentraOS hardware
|
77
|
-
repeat = false, validateFrames = true, loadOptions = {}, onStart, onStop, onFrame, onError } = config;
|
77
|
+
repeat = false, validateFrames = true, loadOptions = {}, onStart, onStop, onFrame, onError, } = config;
|
78
78
|
try {
|
79
79
|
console.log(`🎬 Loading ${frameCount} animation frames from ${basePath}...`);
|
80
80
|
// Load frames with validation
|
81
81
|
const frames = await bitmap_utils_1.BitmapUtils.loadBmpFrames(basePath, frameCount, {
|
82
82
|
validateFrames,
|
83
|
-
...loadOptions
|
83
|
+
...loadOptions,
|
84
84
|
});
|
85
85
|
if (frames.length === 0) {
|
86
|
-
throw new Error(
|
86
|
+
throw new Error("No frames loaded for animation");
|
87
87
|
}
|
88
88
|
console.log(`📚 Animation ready: ${frames.length} frames at ${intervalMs}ms intervals`);
|
89
89
|
// Create enhanced animation with the loaded frames
|
@@ -93,11 +93,11 @@ class AnimationUtils {
|
|
93
93
|
onStart,
|
94
94
|
onStop,
|
95
95
|
onFrame,
|
96
|
-
onError
|
96
|
+
onError,
|
97
97
|
});
|
98
98
|
}
|
99
99
|
catch (error) {
|
100
|
-
const errorMsg = `Failed to create animation: ${error instanceof Error ? error.message :
|
100
|
+
const errorMsg = `Failed to create animation: ${error instanceof Error ? error.message : "Unknown error"}`;
|
101
101
|
console.error(`❌ ${errorMsg}`);
|
102
102
|
if (onError) {
|
103
103
|
onError(errorMsg);
|
@@ -122,9 +122,9 @@ class AnimationUtils {
|
|
122
122
|
* ```
|
123
123
|
*/
|
124
124
|
static createBitmapAnimationFromFrames(session, frames, config = {}) {
|
125
|
-
const { intervalMs = 1750, repeat = false, onStart, onStop, onFrame, onError } = config;
|
125
|
+
const { intervalMs = 1750, repeat = false, onStart, onStop, onFrame, onError, } = config;
|
126
126
|
let isRunning = false;
|
127
|
-
|
127
|
+
const currentFrame = 0;
|
128
128
|
let animationController = null;
|
129
129
|
const controller = {
|
130
130
|
stop: () => {
|
@@ -136,11 +136,11 @@ class AnimationUtils {
|
|
136
136
|
if (onStop) {
|
137
137
|
onStop();
|
138
138
|
}
|
139
|
-
console.log(
|
139
|
+
console.log("🛑 Animation stopped");
|
140
140
|
},
|
141
141
|
isRunning: () => isRunning,
|
142
142
|
getCurrentFrame: () => currentFrame,
|
143
|
-
getTotalFrames: () => frames.length
|
143
|
+
getTotalFrames: () => frames.length,
|
144
144
|
};
|
145
145
|
try {
|
146
146
|
// Start the animation using the session's built-in method
|
@@ -149,7 +149,7 @@ class AnimationUtils {
|
|
149
149
|
if (onStart) {
|
150
150
|
onStart();
|
151
151
|
}
|
152
|
-
console.log(`🎬 Animation started: ${frames.length} frames at ${intervalMs}ms${repeat ?
|
152
|
+
console.log(`🎬 Animation started: ${frames.length} frames at ${intervalMs}ms${repeat ? " (repeating)" : ""}`);
|
153
153
|
// If we have frame callbacks, we need to track timing manually
|
154
154
|
// This is a limitation of the current SDK - we can't hook into individual frame displays
|
155
155
|
if (onFrame) {
|
@@ -177,7 +177,7 @@ class AnimationUtils {
|
|
177
177
|
}
|
178
178
|
}
|
179
179
|
catch (error) {
|
180
|
-
const errorMsg = `Failed to start animation: ${error instanceof Error ? error.message :
|
180
|
+
const errorMsg = `Failed to start animation: ${error instanceof Error ? error.message : "Unknown error"}`;
|
181
181
|
console.error(`❌ ${errorMsg}`);
|
182
182
|
if (onError) {
|
183
183
|
onError(errorMsg);
|
@@ -209,7 +209,8 @@ class AnimationUtils {
|
|
209
209
|
try {
|
210
210
|
console.log(`📽️ Sequence frame ${i + 1}/${sequence.length} (${duration}ms)`);
|
211
211
|
session.layouts.showBitmapView(frame);
|
212
|
-
if (i < sequence.length - 1) {
|
212
|
+
if (i < sequence.length - 1) {
|
213
|
+
// Don't delay after the last frame
|
213
214
|
await this.delay(duration);
|
214
215
|
}
|
215
216
|
}
|
@@ -218,7 +219,7 @@ class AnimationUtils {
|
|
218
219
|
throw error;
|
219
220
|
}
|
220
221
|
}
|
221
|
-
console.log(
|
222
|
+
console.log("✅ Bitmap sequence completed");
|
222
223
|
}
|
223
224
|
/**
|
224
225
|
* Measure animation timing performance
|
@@ -237,7 +238,7 @@ class AnimationUtils {
|
|
237
238
|
static async measureTiming(targetInterval, measureDuration = 10000) {
|
238
239
|
return new Promise((resolve) => {
|
239
240
|
const timestamps = [];
|
240
|
-
|
241
|
+
const startTime = Date.now();
|
241
242
|
const measureInterval = setInterval(() => {
|
242
243
|
timestamps.push(Date.now());
|
243
244
|
}, targetInterval);
|
@@ -248,7 +249,7 @@ class AnimationUtils {
|
|
248
249
|
targetInterval,
|
249
250
|
actualInterval: targetInterval,
|
250
251
|
drift: 0,
|
251
|
-
fps: 1000 / targetInterval
|
252
|
+
fps: 1000 / targetInterval,
|
252
253
|
});
|
253
254
|
return;
|
254
255
|
}
|
@@ -264,7 +265,7 @@ class AnimationUtils {
|
|
264
265
|
targetInterval,
|
265
266
|
actualInterval,
|
266
267
|
drift,
|
267
|
-
fps
|
268
|
+
fps,
|
268
269
|
});
|
269
270
|
}, measureDuration);
|
270
271
|
});
|
@@ -285,17 +286,17 @@ class AnimationUtils {
|
|
285
286
|
*/
|
286
287
|
static getOptimizedConfig(deviceType) {
|
287
288
|
switch (deviceType) {
|
288
|
-
case
|
289
|
+
case "even-realities-g1":
|
289
290
|
return {
|
290
291
|
intervalMs: 1650, // Tested optimal timing for Even Realities G1
|
291
292
|
repeat: false,
|
292
293
|
validateFrames: true,
|
293
294
|
loadOptions: {
|
294
295
|
validateFrames: true,
|
295
|
-
skipMissingFrames: false
|
296
|
-
}
|
296
|
+
skipMissingFrames: false,
|
297
|
+
},
|
297
298
|
};
|
298
|
-
case
|
299
|
+
case "generic":
|
299
300
|
default:
|
300
301
|
return {
|
301
302
|
intervalMs: 1000,
|
@@ -303,8 +304,8 @@ class AnimationUtils {
|
|
303
304
|
validateFrames: true,
|
304
305
|
loadOptions: {
|
305
306
|
validateFrames: true,
|
306
|
-
skipMissingFrames: false
|
307
|
-
}
|
307
|
+
skipMissingFrames: false,
|
308
|
+
},
|
308
309
|
};
|
309
310
|
}
|
310
311
|
}
|
@@ -330,7 +331,7 @@ class AnimationUtils {
|
|
330
331
|
console.log(`📦 Preloading ${frameCount} frames from ${basePath}...`);
|
331
332
|
const frames = await bitmap_utils_1.BitmapUtils.loadBmpFrames(basePath, frameCount, {
|
332
333
|
validateFrames: true,
|
333
|
-
...options
|
334
|
+
...options,
|
334
335
|
});
|
335
336
|
console.log(`✅ Preloaded ${frames.length} frames (${frames.reduce((total, frame) => total + frame.length, 0)} total characters)`);
|
336
337
|
return frames;
|
@@ -70,7 +70,9 @@ export declare class BitmapUtils {
|
|
70
70
|
* session.layouts.showBitmapView(bmpHex);
|
71
71
|
* ```
|
72
72
|
*/
|
73
|
-
static
|
73
|
+
static loadBmpFromFileAsHex(filePath: string): Promise<string>;
|
74
|
+
static convert24BitTo1BitBMP(input24BitBmp: Buffer): Promise<Buffer>;
|
75
|
+
static loadBmpFromDataAsHex(bmpData: Buffer): Promise<string>;
|
74
76
|
/**
|
75
77
|
* Load multiple BMP frames as hex array for animations
|
76
78
|
*
|
@@ -123,7 +125,7 @@ export declare class BitmapUtils {
|
|
123
125
|
* const bufferData = BitmapUtils.convertFormat(base64Data, 'base64', 'buffer');
|
124
126
|
* ```
|
125
127
|
*/
|
126
|
-
static convertFormat(data: string | Buffer, fromFormat:
|
128
|
+
static convertFormat(data: string | Buffer, fromFormat: "hex" | "base64" | "buffer", toFormat: "hex" | "base64" | "buffer"): string | Buffer;
|
127
129
|
/**
|
128
130
|
* Get bitmap information without full validation
|
129
131
|
*
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"bitmap-utils.d.ts","sourceRoot":"","sources":["../../src/utils/bitmap-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;
|
1
|
+
{"version":3,"file":"bitmap-utils.d.ts","sourceRoot":"","sources":["../../src/utils/bitmap-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAMH;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,uCAAuC;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE;QACT,iDAAiD;QACjD,UAAU,CAAC,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC;QAC/C,8BAA8B;QAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,gEAAgE;IAChE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yDAAyD;IACzD,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,8DAA8D;IAC9D,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;GAEG;AACH,qBAAa,WAAW;IACtB;;;;;;;;;;;;OAYG;WACU,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;WAevD,qBAAqB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;WA+G7D,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAiDnE;;;;;;;;;;;;;;;;;;;OAmBG;WACU,aAAa,CACxB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,MAAM,EAAE,CAAC;IAgEpB;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB;IA+F1D;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,aAAa,CAClB,IAAI,EAAE,MAAM,GAAG,MAAM,EACrB,UAAU,EAAE,KAAK,GAAG,QAAQ,GAAG,QAAQ,EACvC,QAAQ,EAAE,KAAK,GAAG,QAAQ,GAAG,QAAQ,GACpC,MAAM,GAAG,MAAM;IA+BlB;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG;QACvC,SAAS,EAAE,MAAM,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,OAAO,CAAC;KACrB;CAsCF"}
|
@@ -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.5",
|
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",
|