@mentra/sdk 2.1.29-beta.2 → 2.1.29
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/server/index.d.ts +70 -5
- package/dist/app/server/index.d.ts.map +1 -1
- package/dist/app/session/device-state.d.ts +83 -0
- package/dist/app/session/device-state.d.ts.map +1 -0
- package/dist/app/session/events.d.ts +9 -0
- package/dist/app/session/events.d.ts.map +1 -1
- package/dist/app/session/index.d.ts +23 -3
- package/dist/app/session/index.d.ts.map +1 -1
- package/dist/app/session/modules/camera.d.ts +5 -43
- package/dist/app/session/modules/camera.d.ts.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +389 -135
- package/dist/index.js.map +16 -14
- package/dist/types/capabilities.d.ts +3 -90
- package/dist/types/capabilities.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/message-types.d.ts +8 -3
- package/dist/types/message-types.d.ts.map +1 -1
- package/dist/types/messages/app-to-cloud.d.ts +15 -1
- package/dist/types/messages/app-to-cloud.d.ts.map +1 -1
- package/dist/types/messages/cloud-to-app.d.ts +14 -1
- package/dist/types/messages/cloud-to-app.d.ts.map +1 -1
- package/dist/types/messages/cloud-to-glasses.d.ts +10 -3
- package/dist/types/messages/cloud-to-glasses.d.ts.map +1 -1
- package/dist/types/messages/glasses-to-cloud.d.ts +20 -2
- package/dist/types/messages/glasses-to-cloud.d.ts.map +1 -1
- package/dist/utils/Observable.d.ts +92 -0
- package/dist/utils/Observable.d.ts.map +1 -0
- package/node_modules/@mentra/types/README.md +134 -0
- package/node_modules/@mentra/types/dist/applet.d.ts +39 -0
- package/node_modules/@mentra/types/dist/applet.d.ts.map +1 -0
- package/node_modules/@mentra/types/dist/applet.js +5 -0
- package/node_modules/@mentra/types/dist/capabilities/even-realities-g1.d.ts +12 -0
- package/node_modules/@mentra/types/dist/capabilities/even-realities-g1.d.ts.map +1 -0
- package/node_modules/@mentra/types/dist/capabilities/even-realities-g1.js +54 -0
- package/node_modules/@mentra/types/dist/capabilities/mentra-live.d.ts +12 -0
- package/node_modules/@mentra/types/dist/capabilities/mentra-live.d.ts.map +1 -0
- package/node_modules/@mentra/types/dist/capabilities/mentra-live.js +94 -0
- package/node_modules/@mentra/types/dist/capabilities/simulated-glasses.d.ts +13 -0
- package/node_modules/@mentra/types/dist/capabilities/simulated-glasses.d.ts.map +1 -0
- package/node_modules/@mentra/types/dist/capabilities/simulated-glasses.js +67 -0
- package/node_modules/@mentra/types/dist/capabilities/vuzix-z100.d.ts +12 -0
- package/node_modules/@mentra/types/dist/capabilities/vuzix-z100.d.ts.map +1 -0
- package/node_modules/@mentra/types/dist/capabilities/vuzix-z100.js +51 -0
- package/node_modules/@mentra/types/dist/cli.d.ts +130 -0
- package/node_modules/@mentra/types/dist/cli.d.ts.map +1 -0
- package/node_modules/@mentra/types/dist/cli.js +7 -0
- package/node_modules/@mentra/types/dist/device.d.ts +32 -0
- package/node_modules/@mentra/types/dist/device.d.ts.map +1 -0
- package/node_modules/@mentra/types/dist/device.js +6 -0
- package/node_modules/@mentra/types/dist/enums.d.ts +34 -0
- package/node_modules/@mentra/types/dist/enums.d.ts.map +1 -0
- package/node_modules/@mentra/types/dist/enums.js +39 -0
- package/node_modules/@mentra/types/dist/hardware.d.ts +141 -0
- package/node_modules/@mentra/types/dist/hardware.d.ts.map +1 -0
- package/node_modules/@mentra/types/dist/hardware.js +33 -0
- package/node_modules/@mentra/types/dist/index.d.ts +18 -0
- package/node_modules/@mentra/types/dist/index.d.ts.map +1 -0
- package/node_modules/@mentra/types/dist/index.js +25 -0
- package/node_modules/@mentra/types/package.json +31 -0
- package/node_modules/@mentra/types/src/applet.ts +51 -0
- package/node_modules/@mentra/types/src/capabilities/even-realities-g1.ts +63 -0
- package/node_modules/@mentra/types/src/capabilities/mentra-live.ts +103 -0
- package/node_modules/@mentra/types/src/capabilities/simulated-glasses.ts +76 -0
- package/node_modules/@mentra/types/src/capabilities/vuzix-z100.ts +60 -0
- package/node_modules/@mentra/types/src/cli.ts +169 -0
- package/node_modules/@mentra/types/src/device.ts +43 -0
- package/node_modules/@mentra/types/src/enums.ts +36 -0
- package/node_modules/@mentra/types/src/hardware.ts +172 -0
- package/node_modules/@mentra/types/src/index.ts +64 -0
- package/node_modules/@mentra/types/tsconfig.json +22 -0
- package/node_modules/@mentra/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +6 -6
- package/dist/display-utils/test/ScrollView.test.d.ts +0 -2
- package/dist/display-utils/test/ScrollView.test.d.ts.map +0 -1
- package/dist/display-utils/test/TextMeasurer.test.d.ts +0 -2
- package/dist/display-utils/test/TextMeasurer.test.d.ts.map +0 -1
- package/dist/display-utils/test/TextWrapper.test.d.ts +0 -2
- package/dist/display-utils/test/TextWrapper.test.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -326,6 +326,8 @@ var init_message_types = __esm(() => {
|
|
|
326
326
|
GlassesToCloudMessageType2["AUDIO_PLAY_RESPONSE"] = "audio_play_response";
|
|
327
327
|
GlassesToCloudMessageType2["RGB_LED_CONTROL_RESPONSE"] = "rgb_led_control_response";
|
|
328
328
|
GlassesToCloudMessageType2["LIVEKIT_INIT"] = "livekit_init";
|
|
329
|
+
GlassesToCloudMessageType2["UDP_REGISTER"] = "udp_register";
|
|
330
|
+
GlassesToCloudMessageType2["UDP_UNREGISTER"] = "udp_unregister";
|
|
329
331
|
})(GlassesToCloudMessageType ||= {});
|
|
330
332
|
((CloudToGlassesMessageType2) => {
|
|
331
333
|
CloudToGlassesMessageType2["CONNECTION_ACK"] = "connection_ack";
|
|
@@ -349,6 +351,7 @@ var init_message_types = __esm(() => {
|
|
|
349
351
|
CloudToGlassesMessageType2["REQUEST_SINGLE_LOCATION"] = "request_single_location";
|
|
350
352
|
CloudToGlassesMessageType2["WEBSOCKET_ERROR"] = "websocket_error";
|
|
351
353
|
CloudToGlassesMessageType2["LIVEKIT_INFO"] = "livekit_info";
|
|
354
|
+
CloudToGlassesMessageType2["UDP_PING_ACK"] = "udp_ping_ack";
|
|
352
355
|
})(CloudToGlassesMessageType ||= {});
|
|
353
356
|
((AppToCloudMessageType2) => {
|
|
354
357
|
AppToCloudMessageType2["CONNECTION_INIT"] = "tpa_connection_init";
|
|
@@ -373,6 +376,7 @@ var init_message_types = __esm(() => {
|
|
|
373
376
|
AppToCloudMessageType2["APP_USER_DISCOVERY"] = "app_user_discovery";
|
|
374
377
|
AppToCloudMessageType2["APP_ROOM_JOIN"] = "app_room_join";
|
|
375
378
|
AppToCloudMessageType2["APP_ROOM_LEAVE"] = "app_room_leave";
|
|
379
|
+
AppToCloudMessageType2["OWNERSHIP_RELEASE"] = "ownership_release";
|
|
376
380
|
})(AppToCloudMessageType ||= {});
|
|
377
381
|
((CloudToAppMessageType2) => {
|
|
378
382
|
CloudToAppMessageType2["CONNECTION_ACK"] = "tpa_connection_ack";
|
|
@@ -380,6 +384,7 @@ var init_message_types = __esm(() => {
|
|
|
380
384
|
CloudToAppMessageType2["APP_STOPPED"] = "app_stopped";
|
|
381
385
|
CloudToAppMessageType2["SETTINGS_UPDATE"] = "settings_update";
|
|
382
386
|
CloudToAppMessageType2["CAPABILITIES_UPDATE"] = "capabilities_update";
|
|
387
|
+
CloudToAppMessageType2["DEVICE_STATE_UPDATE"] = "device_state_update";
|
|
383
388
|
CloudToAppMessageType2["DASHBOARD_MODE_CHANGED"] = "dashboard_mode_changed";
|
|
384
389
|
CloudToAppMessageType2["DASHBOARD_ALWAYS_ON_CHANGED"] = "dashboard_always_on_changed";
|
|
385
390
|
CloudToAppMessageType2["DATA_STREAM"] = "data_stream";
|
|
@@ -735,6 +740,12 @@ function isAudioPlayResponse(message) {
|
|
|
735
740
|
function isLocalTranscription(message) {
|
|
736
741
|
return message.type === "local_transcription" /* LOCAL_TRANSCRIPTION */;
|
|
737
742
|
}
|
|
743
|
+
function isUdpRegister(message) {
|
|
744
|
+
return message.type === "udp_register" /* UDP_REGISTER */;
|
|
745
|
+
}
|
|
746
|
+
function isUdpUnregister(message) {
|
|
747
|
+
return message.type === "udp_unregister" /* UDP_UNREGISTER */;
|
|
748
|
+
}
|
|
738
749
|
// src/types/messages/cloud-to-glasses.ts
|
|
739
750
|
init_message_types();
|
|
740
751
|
function isResponse(message) {
|
|
@@ -829,6 +840,9 @@ function isRtmpStreamRequest(message) {
|
|
|
829
840
|
function isRtmpStreamStopRequest(message) {
|
|
830
841
|
return message.type === "rtmp_stream_stop" /* RTMP_STREAM_STOP */;
|
|
831
842
|
}
|
|
843
|
+
function isOwnershipRelease(message) {
|
|
844
|
+
return message.type === "ownership_release" /* OWNERSHIP_RELEASE */;
|
|
845
|
+
}
|
|
832
846
|
// src/utils/bitmap-utils.ts
|
|
833
847
|
import * as fs from "fs/promises";
|
|
834
848
|
import * as path from "path";
|
|
@@ -1339,6 +1353,9 @@ function isSettingsUpdate2(message) {
|
|
|
1339
1353
|
function isCapabilitiesUpdate(message) {
|
|
1340
1354
|
return message.type === "capabilities_update" /* CAPABILITIES_UPDATE */;
|
|
1341
1355
|
}
|
|
1356
|
+
function isDeviceStateUpdate(message) {
|
|
1357
|
+
return message.type === "device_state_update" /* DEVICE_STATE_UPDATE */;
|
|
1358
|
+
}
|
|
1342
1359
|
function isDataStream(message) {
|
|
1343
1360
|
return message.type === "data_stream" /* DATA_STREAM */;
|
|
1344
1361
|
}
|
|
@@ -1906,6 +1923,9 @@ class EventManager {
|
|
|
1906
1923
|
this.unsubscribe(type);
|
|
1907
1924
|
}
|
|
1908
1925
|
}
|
|
1926
|
+
getRegisteredStreams() {
|
|
1927
|
+
return Array.from(this.handlers.keys());
|
|
1928
|
+
}
|
|
1909
1929
|
emit(event, data) {
|
|
1910
1930
|
try {
|
|
1911
1931
|
this.emitter.emit(event, data);
|
|
@@ -2195,7 +2215,7 @@ class ApiClient {
|
|
|
2195
2215
|
import pino from "pino";
|
|
2196
2216
|
var BETTERSTACK_SOURCE_TOKEN = process.env.BETTERSTACK_SOURCE_TOKEN;
|
|
2197
2217
|
var BETTERSTACK_ENDPOINT = process.env.BETTERSTACK_ENDPOINT || "https://s1311181.eu-nbg-2.betterstackdata.com";
|
|
2198
|
-
var NODE_ENV = "
|
|
2218
|
+
var NODE_ENV = "aryan";
|
|
2199
2219
|
var PORTER_APP_NAME = process.env.PORTER_APP_NAME || "cloud-local";
|
|
2200
2220
|
var LOG_LEVEL = NODE_ENV === "production" ? "info" : "debug";
|
|
2201
2221
|
var streams2 = [];
|
|
@@ -2635,7 +2655,6 @@ class CameraModule {
|
|
|
2635
2655
|
packageName;
|
|
2636
2656
|
sessionId;
|
|
2637
2657
|
logger;
|
|
2638
|
-
pendingPhotoRequests = new Map;
|
|
2639
2658
|
isStreaming = false;
|
|
2640
2659
|
currentStreamUrl;
|
|
2641
2660
|
currentStreamState;
|
|
@@ -2652,9 +2671,15 @@ class CameraModule {
|
|
|
2652
2671
|
const baseUrl = this.session?.getHttpsServerUrl?.() || "";
|
|
2653
2672
|
cameraWarnLog(baseUrl, this.packageName, "requestPhoto");
|
|
2654
2673
|
try {
|
|
2655
|
-
console.log("DEBUG: requestPhoto options:", options);
|
|
2656
2674
|
const requestId = `photo_req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
2657
|
-
this.
|
|
2675
|
+
this.session.appServer.registerPhotoRequest(requestId, {
|
|
2676
|
+
userId: this.session.userId,
|
|
2677
|
+
sessionId: this.sessionId,
|
|
2678
|
+
session: this.session,
|
|
2679
|
+
resolve,
|
|
2680
|
+
reject: (error) => reject(error.message),
|
|
2681
|
+
timestamp: Date.now()
|
|
2682
|
+
});
|
|
2658
2683
|
const message = {
|
|
2659
2684
|
type: "photo_request" /* PHOTO_REQUEST */,
|
|
2660
2685
|
packageName: this.packageName,
|
|
@@ -2676,91 +2701,41 @@ class CameraModule {
|
|
|
2676
2701
|
}, `\uD83D\uDCF8 Photo request sent`);
|
|
2677
2702
|
if (options?.customWebhookUrl) {
|
|
2678
2703
|
this.logger.info({ requestId, customWebhookUrl: options.customWebhookUrl }, `\uD83D\uDCF8 Using custom webhook URL - resolving promise immediately since photo will be uploaded directly to custom endpoint`);
|
|
2679
|
-
const
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2704
|
+
const pending = this.session.appServer.completePhotoRequest(requestId);
|
|
2705
|
+
if (pending) {
|
|
2706
|
+
const mockPhotoData = {
|
|
2707
|
+
buffer: Buffer.from([]),
|
|
2708
|
+
mimeType: "image/jpeg",
|
|
2709
|
+
filename: "photo.jpg",
|
|
2710
|
+
requestId,
|
|
2711
|
+
size: 0,
|
|
2712
|
+
timestamp: new Date
|
|
2713
|
+
};
|
|
2714
|
+
pending.resolve(mockPhotoData);
|
|
2715
|
+
}
|
|
2689
2716
|
return;
|
|
2690
2717
|
}
|
|
2691
|
-
const timeoutMs = 30000;
|
|
2692
|
-
if (this.session && this.session.resources) {
|
|
2693
|
-
this.session.resources.setTimeout(() => {
|
|
2694
|
-
if (this.pendingPhotoRequests.has(requestId)) {
|
|
2695
|
-
this.pendingPhotoRequests.get(requestId).reject("Photo request timed out");
|
|
2696
|
-
this.pendingPhotoRequests.delete(requestId);
|
|
2697
|
-
this.logger.warn({ requestId }, `\uD83D\uDCF8 Photo request timed out`);
|
|
2698
|
-
}
|
|
2699
|
-
}, timeoutMs);
|
|
2700
|
-
} else {
|
|
2701
|
-
setTimeout(() => {
|
|
2702
|
-
if (this.pendingPhotoRequests.has(requestId)) {
|
|
2703
|
-
this.pendingPhotoRequests.get(requestId).reject("Photo request timed out");
|
|
2704
|
-
this.pendingPhotoRequests.delete(requestId);
|
|
2705
|
-
this.logger.warn({ requestId }, `\uD83D\uDCF8 Photo request timed out`);
|
|
2706
|
-
}
|
|
2707
|
-
}, timeoutMs);
|
|
2708
|
-
}
|
|
2709
2718
|
} catch (error) {
|
|
2710
2719
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2711
2720
|
reject(`Failed to request photo: ${errorMessage}`);
|
|
2712
2721
|
}
|
|
2713
2722
|
});
|
|
2714
2723
|
}
|
|
2715
|
-
handlePhotoReceived(photoData) {
|
|
2716
|
-
const { requestId } = photoData;
|
|
2717
|
-
const pendingRequest = this.pendingPhotoRequests.get(requestId);
|
|
2718
|
-
if (pendingRequest) {
|
|
2719
|
-
this.logger.info({ requestId }, `\uD83D\uDCF8 Photo received for request ${requestId}`);
|
|
2720
|
-
pendingRequest.resolve(photoData);
|
|
2721
|
-
this.pendingPhotoRequests.delete(requestId);
|
|
2722
|
-
} else {
|
|
2723
|
-
this.logger.warn({ requestId }, `\uD83D\uDCF8 Received photo for unknown request ID: ${requestId}`);
|
|
2724
|
-
}
|
|
2725
|
-
}
|
|
2726
|
-
handlePhotoError(errorResponse) {
|
|
2727
|
-
const { requestId, error } = errorResponse;
|
|
2728
|
-
const pendingRequest = this.pendingPhotoRequests.get(requestId);
|
|
2729
|
-
if (pendingRequest) {
|
|
2730
|
-
this.logger.error({ requestId, errorCode: error.code, errorMessage: error.message }, `\uD83D\uDCF8 Photo capture failed: ${error.code} - ${error.message}`);
|
|
2731
|
-
pendingRequest.reject(`${error.code}: ${error.message}`);
|
|
2732
|
-
this.pendingPhotoRequests.delete(requestId);
|
|
2733
|
-
} else {
|
|
2734
|
-
this.logger.warn({ requestId, errorCode: error.code, errorMessage: error.message }, `\uD83D\uDCF8 Received photo error for unknown request ID: ${requestId}`);
|
|
2735
|
-
}
|
|
2736
|
-
}
|
|
2737
2724
|
hasPhotoPendingRequest(requestId) {
|
|
2738
|
-
return this.
|
|
2739
|
-
}
|
|
2740
|
-
getPhotoPendingRequestCount() {
|
|
2741
|
-
return this.pendingPhotoRequests.size;
|
|
2742
|
-
}
|
|
2743
|
-
getPhotoPendingRequestIds() {
|
|
2744
|
-
return Array.from(this.pendingPhotoRequests.keys());
|
|
2725
|
+
return this.session.appServer.getPhotoRequest(requestId) !== undefined;
|
|
2745
2726
|
}
|
|
2746
2727
|
cancelPhotoRequest(requestId) {
|
|
2747
|
-
const
|
|
2748
|
-
if (
|
|
2749
|
-
|
|
2750
|
-
this.pendingPhotoRequests.delete(requestId);
|
|
2728
|
+
const pending = this.session.appServer.completePhotoRequest(requestId);
|
|
2729
|
+
if (pending) {
|
|
2730
|
+
pending.reject(new Error("Photo request cancelled"));
|
|
2751
2731
|
this.logger.info({ requestId }, `\uD83D\uDCF8 Photo request cancelled`);
|
|
2752
2732
|
return true;
|
|
2753
2733
|
}
|
|
2754
2734
|
return false;
|
|
2755
2735
|
}
|
|
2756
2736
|
cancelAllPhotoRequests() {
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
reject("Photo request cancelled - session cleanup");
|
|
2760
|
-
this.logger.info({ requestId }, `\uD83D\uDCF8 Photo request cancelled during cleanup`);
|
|
2761
|
-
}
|
|
2762
|
-
this.pendingPhotoRequests.clear();
|
|
2763
|
-
return count;
|
|
2737
|
+
this.logger.debug(`\uD83D\uDCF8 cancelAllPhotoRequests called - cleanup now happens at AppServer level`);
|
|
2738
|
+
return 0;
|
|
2764
2739
|
}
|
|
2765
2740
|
async startStream(options) {
|
|
2766
2741
|
this.logger.info({ rtmpUrl: options.rtmpUrl }, `\uD83D\uDCF9 RTMP stream request starting`);
|
|
@@ -3527,7 +3502,151 @@ class SimpleStorage {
|
|
|
3527
3502
|
}
|
|
3528
3503
|
}
|
|
3529
3504
|
|
|
3505
|
+
// src/utils/Observable.ts
|
|
3506
|
+
class Observable {
|
|
3507
|
+
_value;
|
|
3508
|
+
_listeners = new Set;
|
|
3509
|
+
_initialized = false;
|
|
3510
|
+
constructor(initialValue) {
|
|
3511
|
+
this._value = initialValue;
|
|
3512
|
+
}
|
|
3513
|
+
get value() {
|
|
3514
|
+
return this._value;
|
|
3515
|
+
}
|
|
3516
|
+
valueOf() {
|
|
3517
|
+
return this._value;
|
|
3518
|
+
}
|
|
3519
|
+
toString() {
|
|
3520
|
+
return String(this._value);
|
|
3521
|
+
}
|
|
3522
|
+
[Symbol.toPrimitive](hint) {
|
|
3523
|
+
if (hint === "string") {
|
|
3524
|
+
return String(this._value);
|
|
3525
|
+
}
|
|
3526
|
+
return this._value;
|
|
3527
|
+
}
|
|
3528
|
+
onChange(callback) {
|
|
3529
|
+
this._listeners.add(callback);
|
|
3530
|
+
if (this._initialized) {
|
|
3531
|
+
callback(this._value);
|
|
3532
|
+
}
|
|
3533
|
+
return () => this._listeners.delete(callback);
|
|
3534
|
+
}
|
|
3535
|
+
setValue(value) {
|
|
3536
|
+
const isFirstInit = !this._initialized;
|
|
3537
|
+
if (isFirstInit) {
|
|
3538
|
+
this._initialized = true;
|
|
3539
|
+
}
|
|
3540
|
+
if (isFirstInit || this._value !== value) {
|
|
3541
|
+
this._value = value;
|
|
3542
|
+
this._listeners.forEach((cb) => {
|
|
3543
|
+
try {
|
|
3544
|
+
cb(value);
|
|
3545
|
+
} catch (error) {
|
|
3546
|
+
console.error("Error in Observable onChange callback:", error);
|
|
3547
|
+
}
|
|
3548
|
+
});
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
get listenerCount() {
|
|
3552
|
+
return this._listeners.size;
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
// src/app/session/device-state.ts
|
|
3557
|
+
class DeviceState {
|
|
3558
|
+
wifiConnected;
|
|
3559
|
+
wifiSsid;
|
|
3560
|
+
wifiLocalIp;
|
|
3561
|
+
batteryLevel;
|
|
3562
|
+
charging;
|
|
3563
|
+
caseBatteryLevel;
|
|
3564
|
+
caseCharging;
|
|
3565
|
+
caseOpen;
|
|
3566
|
+
caseRemoved;
|
|
3567
|
+
hotspotEnabled;
|
|
3568
|
+
hotspotSsid;
|
|
3569
|
+
connected;
|
|
3570
|
+
modelName;
|
|
3571
|
+
appSession;
|
|
3572
|
+
constructor(appSession) {
|
|
3573
|
+
this.appSession = appSession;
|
|
3574
|
+
this.wifiConnected = new Observable(false);
|
|
3575
|
+
this.wifiSsid = new Observable(null);
|
|
3576
|
+
this.wifiLocalIp = new Observable(null);
|
|
3577
|
+
this.batteryLevel = new Observable(null);
|
|
3578
|
+
this.charging = new Observable(null);
|
|
3579
|
+
this.caseBatteryLevel = new Observable(null);
|
|
3580
|
+
this.caseCharging = new Observable(null);
|
|
3581
|
+
this.caseOpen = new Observable(null);
|
|
3582
|
+
this.caseRemoved = new Observable(null);
|
|
3583
|
+
this.hotspotEnabled = new Observable(null);
|
|
3584
|
+
this.hotspotSsid = new Observable(null);
|
|
3585
|
+
this.connected = new Observable(false);
|
|
3586
|
+
this.modelName = new Observable(null);
|
|
3587
|
+
}
|
|
3588
|
+
updateFromMessage(state) {
|
|
3589
|
+
if (state.connected !== undefined) {
|
|
3590
|
+
this.connected.setValue(state.connected);
|
|
3591
|
+
}
|
|
3592
|
+
if (state.modelName !== undefined) {
|
|
3593
|
+
this.modelName.setValue(state.modelName);
|
|
3594
|
+
}
|
|
3595
|
+
if (state.wifiConnected !== undefined) {
|
|
3596
|
+
this.wifiConnected.setValue(state.wifiConnected);
|
|
3597
|
+
}
|
|
3598
|
+
if (state.wifiSsid !== undefined) {
|
|
3599
|
+
this.wifiSsid.setValue(state.wifiSsid ?? null);
|
|
3600
|
+
}
|
|
3601
|
+
if (state.wifiLocalIp !== undefined) {
|
|
3602
|
+
this.wifiLocalIp.setValue(state.wifiLocalIp ?? null);
|
|
3603
|
+
}
|
|
3604
|
+
if (state.batteryLevel !== undefined) {
|
|
3605
|
+
this.batteryLevel.setValue(state.batteryLevel ?? null);
|
|
3606
|
+
}
|
|
3607
|
+
if (state.charging !== undefined) {
|
|
3608
|
+
this.charging.setValue(state.charging ?? null);
|
|
3609
|
+
}
|
|
3610
|
+
if (state.caseBatteryLevel !== undefined) {
|
|
3611
|
+
this.caseBatteryLevel.setValue(state.caseBatteryLevel ?? null);
|
|
3612
|
+
}
|
|
3613
|
+
if (state.caseCharging !== undefined) {
|
|
3614
|
+
this.caseCharging.setValue(state.caseCharging ?? null);
|
|
3615
|
+
}
|
|
3616
|
+
if (state.caseOpen !== undefined) {
|
|
3617
|
+
this.caseOpen.setValue(state.caseOpen ?? null);
|
|
3618
|
+
}
|
|
3619
|
+
if (state.caseRemoved !== undefined) {
|
|
3620
|
+
this.caseRemoved.setValue(state.caseRemoved ?? null);
|
|
3621
|
+
}
|
|
3622
|
+
if (state.hotspotEnabled !== undefined) {
|
|
3623
|
+
this.hotspotEnabled.setValue(state.hotspotEnabled ?? null);
|
|
3624
|
+
}
|
|
3625
|
+
if (state.hotspotSsid !== undefined) {
|
|
3626
|
+
this.hotspotSsid.setValue(state.hotspotSsid ?? null);
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
getSnapshot() {
|
|
3630
|
+
return {
|
|
3631
|
+
connected: this.connected.value,
|
|
3632
|
+
modelName: this.modelName.value ?? undefined,
|
|
3633
|
+
wifiConnected: this.wifiConnected.value,
|
|
3634
|
+
wifiSsid: this.wifiSsid.value ?? undefined,
|
|
3635
|
+
wifiLocalIp: this.wifiLocalIp.value ?? undefined,
|
|
3636
|
+
batteryLevel: this.batteryLevel.value ?? undefined,
|
|
3637
|
+
charging: this.charging.value ?? undefined,
|
|
3638
|
+
caseBatteryLevel: this.caseBatteryLevel.value ?? undefined,
|
|
3639
|
+
caseCharging: this.caseCharging.value ?? undefined,
|
|
3640
|
+
caseOpen: this.caseOpen.value ?? undefined,
|
|
3641
|
+
caseRemoved: this.caseRemoved.value ?? undefined,
|
|
3642
|
+
hotspotEnabled: this.hotspotEnabled.value ?? undefined,
|
|
3643
|
+
hotspotSsid: this.hotspotSsid.value ?? undefined
|
|
3644
|
+
};
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3530
3648
|
// src/app/session/index.ts
|
|
3649
|
+
var SDK_SUBSCRIPTION_PATCH = "bug007-fix-v2";
|
|
3531
3650
|
var APP_TO_APP_EVENT_TYPES = [
|
|
3532
3651
|
"app_message_received",
|
|
3533
3652
|
"app_user_joined",
|
|
@@ -3541,7 +3660,7 @@ class AppSession {
|
|
|
3541
3660
|
ws = null;
|
|
3542
3661
|
sessionId = null;
|
|
3543
3662
|
reconnectAttempts = 0;
|
|
3544
|
-
|
|
3663
|
+
terminated = false;
|
|
3545
3664
|
streamRates = new Map;
|
|
3546
3665
|
resources = new ResourceTracker;
|
|
3547
3666
|
settingsData = [];
|
|
@@ -3560,6 +3679,7 @@ class AppSession {
|
|
|
3560
3679
|
led;
|
|
3561
3680
|
audio;
|
|
3562
3681
|
simpleStorage;
|
|
3682
|
+
device;
|
|
3563
3683
|
appServer;
|
|
3564
3684
|
logger;
|
|
3565
3685
|
userId;
|
|
@@ -3609,18 +3729,14 @@ class AppSession {
|
|
|
3609
3729
|
this.layouts = new LayoutManager(config.packageName, this.send.bind(this));
|
|
3610
3730
|
this.settings = new SettingsManager(this.settingsData, this.config.packageName, this.config.mentraOSWebsocketUrl, this.sessionId ?? undefined, async (streams3) => {
|
|
3611
3731
|
this.logger.debug({ streams: JSON.stringify(streams3) }, `[AppSession] subscribeFn called for streams`);
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
this.logger.debug(`[AppSession] Already subscribed to stream '${stream}'.`);
|
|
3618
|
-
}
|
|
3619
|
-
});
|
|
3620
|
-
this.logger.debug({ subscriptions: JSON.stringify(Array.from(this.subscriptions)) }, `[AppSession] Current subscriptions after subscribeFn`);
|
|
3732
|
+
const currentHandlerStreams = this.events.getRegisteredStreams();
|
|
3733
|
+
this.logger.debug({
|
|
3734
|
+
requestedStreams: JSON.stringify(streams3),
|
|
3735
|
+
currentHandlerStreams: JSON.stringify(currentHandlerStreams)
|
|
3736
|
+
}, `[AppSession] subscribeFn: requested streams vs current handler streams`);
|
|
3621
3737
|
if (this.ws?.readyState === 1) {
|
|
3622
3738
|
this.updateSubscriptions();
|
|
3623
|
-
this.logger.debug(`[AppSession] Sent updated subscriptions to cloud
|
|
3739
|
+
this.logger.debug(`[AppSession] Sent updated subscriptions to cloud (derived from handlers).`);
|
|
3624
3740
|
} else {
|
|
3625
3741
|
this.logger.debug(`[AppSession] WebSocket not open, will send subscriptions when connected.`);
|
|
3626
3742
|
}
|
|
@@ -3631,6 +3747,7 @@ class AppSession {
|
|
|
3631
3747
|
this.led = new LedModule(this, this.config.packageName, this.sessionId || "unknown-session-id", this.logger.child({ module: "led" }));
|
|
3632
3748
|
this.audio = new AudioManager(this, this.config.packageName, this.sessionId || "unknown-session-id", this.logger.child({ module: "audio" }));
|
|
3633
3749
|
this.simpleStorage = new SimpleStorage(this);
|
|
3750
|
+
this.device = { state: new DeviceState(this) };
|
|
3634
3751
|
this.location = new LocationManager(this);
|
|
3635
3752
|
}
|
|
3636
3753
|
getSessionId() {
|
|
@@ -3697,7 +3814,6 @@ class AppSession {
|
|
|
3697
3814
|
this.logger.warn(`[AppSession] Attempted to subscribe to App-to-App event type '${type}', which is not a valid stream. Use the event handler (e.g., onAppMessage) instead.`);
|
|
3698
3815
|
return;
|
|
3699
3816
|
}
|
|
3700
|
-
this.subscriptions.add(type);
|
|
3701
3817
|
if (rate) {
|
|
3702
3818
|
this.streamRates.set(type, rate);
|
|
3703
3819
|
}
|
|
@@ -3716,7 +3832,6 @@ class AppSession {
|
|
|
3716
3832
|
this.logger.warn(`[AppSession] Attempted to unsubscribe from App-to-App event type '${type}', which is not a valid stream.`);
|
|
3717
3833
|
return;
|
|
3718
3834
|
}
|
|
3719
|
-
this.subscriptions.delete(type);
|
|
3720
3835
|
this.streamRates.delete(type);
|
|
3721
3836
|
if (this.ws?.readyState === 1) {
|
|
3722
3837
|
this.updateSubscriptions();
|
|
@@ -3840,11 +3955,15 @@ class AppSession {
|
|
|
3840
3955
|
const isUserSessionEnded = reason && reason.includes("User session ended");
|
|
3841
3956
|
this.logger.debug(`\uD83D\uDD0C [${this.config.packageName}] WebSocket closed with code ${code}${reasonStr}`);
|
|
3842
3957
|
this.logger.debug(`\uD83D\uDD0C [${this.config.packageName}] isNormalClosure: ${isNormalClosure}, isManualStop: ${isManualStop}, isUserSessionEnded: ${isUserSessionEnded}`);
|
|
3843
|
-
if (
|
|
3958
|
+
if (isUserSessionEnded) {
|
|
3959
|
+
this.terminated = true;
|
|
3960
|
+
this.logger.info(`\uD83D\uDED1 [${this.config.packageName}] User session ended - marking as terminated, no reconnection allowed`);
|
|
3961
|
+
}
|
|
3962
|
+
if (!isNormalClosure && !isManualStop && !this.terminated) {
|
|
3844
3963
|
this.logger.warn(`\uD83D\uDD0C [${this.config.packageName}] Abnormal closure detected, attempting reconnection`);
|
|
3845
3964
|
this.handleReconnection();
|
|
3846
3965
|
} else {
|
|
3847
|
-
this.logger.debug(`\uD83D\uDD0C [${this.config.packageName}] Normal closure detected, not attempting reconnection`);
|
|
3966
|
+
this.logger.debug(`\uD83D\uDD0C [${this.config.packageName}] Normal/terminated closure detected, not attempting reconnection (terminated: ${this.terminated})`);
|
|
3848
3967
|
}
|
|
3849
3968
|
if (isUserSessionEnded) {
|
|
3850
3969
|
this.logger.info(`\uD83D\uDED1 [${this.config.packageName}] User session ended - emitting disconnected event with sessionEnded flag`);
|
|
@@ -3908,7 +4027,26 @@ class AppSession {
|
|
|
3908
4027
|
}
|
|
3909
4028
|
});
|
|
3910
4029
|
}
|
|
3911
|
-
async
|
|
4030
|
+
async releaseOwnership(reason) {
|
|
4031
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
4032
|
+
this.logger.debug(`[${this.config.packageName}] Cannot release ownership - WebSocket not open`);
|
|
4033
|
+
return;
|
|
4034
|
+
}
|
|
4035
|
+
const message = {
|
|
4036
|
+
type: "ownership_release" /* OWNERSHIP_RELEASE */,
|
|
4037
|
+
packageName: this.config.packageName,
|
|
4038
|
+
sessionId: this.sessionId || "",
|
|
4039
|
+
reason,
|
|
4040
|
+
timestamp: new Date
|
|
4041
|
+
};
|
|
4042
|
+
this.logger.info({ reason, sessionId: this.sessionId }, `\uD83D\uDD04 [${this.config.packageName}] Releasing ownership: ${reason}`);
|
|
4043
|
+
this.send(message);
|
|
4044
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
4045
|
+
}
|
|
4046
|
+
async disconnect(options) {
|
|
4047
|
+
if (options?.releaseOwnership && options?.reason) {
|
|
4048
|
+
await this.releaseOwnership(options.reason);
|
|
4049
|
+
}
|
|
3912
4050
|
try {
|
|
3913
4051
|
await this.simpleStorage.flush();
|
|
3914
4052
|
console.log("SimpleStorage flushed on disconnect");
|
|
@@ -3924,7 +4062,6 @@ class AppSession {
|
|
|
3924
4062
|
this.resources.dispose();
|
|
3925
4063
|
this.ws = null;
|
|
3926
4064
|
this.sessionId = null;
|
|
3927
|
-
this.subscriptions.clear();
|
|
3928
4065
|
this.reconnectAttempts = 0;
|
|
3929
4066
|
}
|
|
3930
4067
|
getSettings() {
|
|
@@ -3945,11 +4082,14 @@ class AppSession {
|
|
|
3945
4082
|
if (!this.subscriptionSettingsHandler)
|
|
3946
4083
|
return;
|
|
3947
4084
|
try {
|
|
3948
|
-
const
|
|
3949
|
-
this.
|
|
3950
|
-
|
|
3951
|
-
this.
|
|
3952
|
-
|
|
4085
|
+
const settingsSubscriptions = this.subscriptionSettingsHandler(this.settingsData);
|
|
4086
|
+
const handlerStreams = this.events.getRegisteredStreams();
|
|
4087
|
+
if (settingsSubscriptions.length !== handlerStreams.length) {
|
|
4088
|
+
this.logger.warn({
|
|
4089
|
+
settingsSubscriptions: JSON.stringify(settingsSubscriptions),
|
|
4090
|
+
handlerStreams: JSON.stringify(handlerStreams)
|
|
4091
|
+
}, `[AppSession] Settings-based subscriptions (${settingsSubscriptions.length}) differ from handler-based subscriptions (${handlerStreams.length}). ` + `Subscriptions are now derived from handlers. Ensure handlers are registered for desired streams.`);
|
|
4092
|
+
}
|
|
3953
4093
|
if (this.ws && this.ws.readyState === 1) {
|
|
3954
4094
|
this.updateSubscriptions();
|
|
3955
4095
|
}
|
|
@@ -4079,6 +4219,8 @@ class AppSession {
|
|
|
4079
4219
|
this.logger.debug(`[AppSession] No capabilities provided in CONNECTION_ACK`);
|
|
4080
4220
|
}
|
|
4081
4221
|
this.events.emit("connected", this.settingsData);
|
|
4222
|
+
const handlerCount = this.events.getRegisteredStreams().length;
|
|
4223
|
+
this.logger.info({ patch: SDK_SUBSCRIPTION_PATCH, handlerCount }, `[AppSession] \uD83D\uDD27 SDK Patch Active: ${SDK_SUBSCRIPTION_PATCH} - Subscriptions derived from ${handlerCount} handler(s)`);
|
|
4082
4224
|
this.updateSubscriptions();
|
|
4083
4225
|
if (this.shouldUpdateSubscriptionsOnSettingsChange && this.settingsData.length > 0) {
|
|
4084
4226
|
this.updateSubscriptionsFromSettings();
|
|
@@ -4087,28 +4229,33 @@ class AppSession {
|
|
|
4087
4229
|
const errorMessage = message.message || "Unknown connection error";
|
|
4088
4230
|
this.events.emit("error", new Error(errorMessage));
|
|
4089
4231
|
} else if (message.type === "audio_chunk" /* AUDIO_CHUNK */) {
|
|
4090
|
-
|
|
4232
|
+
const hasAudioHandler = this.events.getRegisteredStreams().includes("audio_chunk" /* AUDIO_CHUNK */);
|
|
4233
|
+
if (hasAudioHandler) {
|
|
4091
4234
|
this.events.emit("audio_chunk" /* AUDIO_CHUNK */, message);
|
|
4092
4235
|
}
|
|
4093
4236
|
} else if (isDataStream(message) && message.streamType === "glasses_connection_state" /* GLASSES_CONNECTION_STATE */) {
|
|
4094
4237
|
this.glassesConnectionState = message.data;
|
|
4095
|
-
|
|
4238
|
+
const hasGlassesStateHandler = this.events.getRegisteredStreams().includes("glasses_connection_state" /* GLASSES_CONNECTION_STATE */);
|
|
4239
|
+
if (hasGlassesStateHandler) {
|
|
4096
4240
|
const sanitizedData = this.sanitizeEventData("glasses_connection_state" /* GLASSES_CONNECTION_STATE */, message.data);
|
|
4097
4241
|
this.events.emit("glasses_connection_state" /* GLASSES_CONNECTION_STATE */, sanitizedData);
|
|
4098
4242
|
}
|
|
4099
4243
|
} else if (isDataStream(message)) {
|
|
4100
4244
|
const messageStreamType = message.streamType;
|
|
4101
|
-
|
|
4245
|
+
const hasHandler = this.events.getRegisteredStreams().includes(messageStreamType);
|
|
4246
|
+
if (messageStreamType && hasHandler) {
|
|
4102
4247
|
const sanitizedData = this.sanitizeEventData(messageStreamType, message.data);
|
|
4103
4248
|
this.events.emit(messageStreamType, sanitizedData);
|
|
4104
4249
|
}
|
|
4105
4250
|
} else if (isRtmpStreamStatus2(message)) {
|
|
4106
|
-
|
|
4251
|
+
const hasRtmpHandler = this.events.getRegisteredStreams().includes("rtmp_stream_status" /* RTMP_STREAM_STATUS */);
|
|
4252
|
+
if (hasRtmpHandler) {
|
|
4107
4253
|
this.events.emit("rtmp_stream_status" /* RTMP_STREAM_STATUS */, message);
|
|
4108
4254
|
}
|
|
4109
4255
|
this.camera.updateStreamState(message);
|
|
4110
4256
|
} else if (isManagedStreamStatus(message)) {
|
|
4111
|
-
|
|
4257
|
+
const hasManagedStreamHandler = this.events.getRegisteredStreams().includes("managed_stream_status" /* MANAGED_STREAM_STATUS */);
|
|
4258
|
+
if (hasManagedStreamHandler) {
|
|
4112
4259
|
this.events.emit("managed_stream_status" /* MANAGED_STREAM_STATUS */, message);
|
|
4113
4260
|
}
|
|
4114
4261
|
this.camera.handleManagedStreamStatus(message);
|
|
@@ -4139,6 +4286,12 @@ class AppSession {
|
|
|
4139
4286
|
modelName: capabilitiesMessage.modelName,
|
|
4140
4287
|
timestamp: capabilitiesMessage.timestamp
|
|
4141
4288
|
});
|
|
4289
|
+
} else if (isDeviceStateUpdate(message)) {
|
|
4290
|
+
this.device.state.updateFromMessage(message.state);
|
|
4291
|
+
this.logger.debug({
|
|
4292
|
+
changedFields: Object.keys(message.state),
|
|
4293
|
+
fullSnapshot: message.fullSnapshot
|
|
4294
|
+
}, `[AppSession] Device state updated via WebSocket`);
|
|
4142
4295
|
} else if (isAppStopped(message)) {
|
|
4143
4296
|
const reason = message.reason || "unknown";
|
|
4144
4297
|
const displayReason = `App stopped: ${reason}`;
|
|
@@ -4245,7 +4398,8 @@ class AppSession {
|
|
|
4245
4398
|
}
|
|
4246
4399
|
handleBinaryMessage(buffer) {
|
|
4247
4400
|
try {
|
|
4248
|
-
|
|
4401
|
+
const hasAudioHandler = this.events.getRegisteredStreams().includes("audio_chunk" /* AUDIO_CHUNK */);
|
|
4402
|
+
if (!hasAudioHandler) {
|
|
4249
4403
|
return;
|
|
4250
4404
|
}
|
|
4251
4405
|
if (!buffer || buffer.byteLength === 0) {
|
|
@@ -4318,8 +4472,9 @@ class AppSession {
|
|
|
4318
4472
|
this.send(message);
|
|
4319
4473
|
}
|
|
4320
4474
|
updateSubscriptions() {
|
|
4321
|
-
|
|
4322
|
-
|
|
4475
|
+
const derivedSubscriptions = this.events.getRegisteredStreams();
|
|
4476
|
+
this.logger.info({ subscriptions: JSON.stringify(derivedSubscriptions) }, `[AppSession] updateSubscriptions: sending ${derivedSubscriptions.length} subscriptions to cloud (derived from handlers)`);
|
|
4477
|
+
const subscriptionPayload = derivedSubscriptions.map((stream) => {
|
|
4323
4478
|
const rate = this.streamRates.get(stream);
|
|
4324
4479
|
if (rate && stream === "location_stream" /* LOCATION_STREAM */) {
|
|
4325
4480
|
return { stream: "location_stream", rate };
|
|
@@ -4336,6 +4491,10 @@ class AppSession {
|
|
|
4336
4491
|
this.send(message);
|
|
4337
4492
|
}
|
|
4338
4493
|
async handleReconnection() {
|
|
4494
|
+
if (this.terminated) {
|
|
4495
|
+
this.logger.info(`\uD83D\uDD04 Reconnection skipped: session was terminated (User session ended). ` + `If cloud restarts app, onSession will be called with fresh handlers.`);
|
|
4496
|
+
return;
|
|
4497
|
+
}
|
|
4339
4498
|
if (!this.config.autoReconnect || !this.sessionId) {
|
|
4340
4499
|
this.logger.debug(`\uD83D\uDD04 Reconnection skipped: autoReconnect=${this.config.autoReconnect}, sessionId=${this.sessionId ? "valid" : "invalid"}`);
|
|
4341
4500
|
return;
|
|
@@ -4415,7 +4574,12 @@ class AppSession {
|
|
|
4415
4574
|
throw new Error(`Failed to send message: ${errorMessage}`);
|
|
4416
4575
|
}
|
|
4417
4576
|
} catch (error) {
|
|
4418
|
-
|
|
4577
|
+
const isDisconnectError = error instanceof Error && (error.message.includes("WebSocket not connected") || error.message.includes("CLOSED") || error.message.includes("CLOSING"));
|
|
4578
|
+
if (isDisconnectError) {
|
|
4579
|
+
this.logger.debug(error, "Message send skipped - session disconnected");
|
|
4580
|
+
} else {
|
|
4581
|
+
this.logger.error(error, "Message send error");
|
|
4582
|
+
}
|
|
4419
4583
|
if (error instanceof Error) {
|
|
4420
4584
|
this.events.emit("error", error);
|
|
4421
4585
|
} else {
|
|
@@ -4872,6 +5036,7 @@ class AppServer {
|
|
|
4872
5036
|
activeSessionsByUserId = new Map;
|
|
4873
5037
|
cleanupHandlers = [];
|
|
4874
5038
|
appInstructions = null;
|
|
5039
|
+
pendingPhotoRequests = new Map;
|
|
4875
5040
|
logger;
|
|
4876
5041
|
constructor(config) {
|
|
4877
5042
|
this.config = config;
|
|
@@ -4968,10 +5133,10 @@ class AppServer {
|
|
|
4968
5133
|
});
|
|
4969
5134
|
});
|
|
4970
5135
|
}
|
|
4971
|
-
stop() {
|
|
5136
|
+
async stop() {
|
|
4972
5137
|
this.logger.info(`
|
|
4973
5138
|
\uD83D\uDED1 Shutting down...`);
|
|
4974
|
-
this.cleanup();
|
|
5139
|
+
await this.cleanup();
|
|
4975
5140
|
process.exit(0);
|
|
4976
5141
|
}
|
|
4977
5142
|
generateToken(userId, sessionId, secretKey) {
|
|
@@ -5046,6 +5211,23 @@ class AppServer {
|
|
|
5046
5211
|
this.logger.info({ userId }, `\uD83D\uDDE3️ Received session request for user ${userId}, session ${sessionId}
|
|
5047
5212
|
|
|
5048
5213
|
`);
|
|
5214
|
+
const existingSession = this.activeSessions.get(sessionId);
|
|
5215
|
+
if (existingSession) {
|
|
5216
|
+
this.logger.info({ sessionId, userId }, `\uD83D\uDD04 Existing session found for ${sessionId} - sending OWNERSHIP_RELEASE and disconnecting before new connection`);
|
|
5217
|
+
try {
|
|
5218
|
+
await existingSession.releaseOwnership("switching_clouds");
|
|
5219
|
+
} catch (error) {
|
|
5220
|
+
this.logger.warn({ error, sessionId }, `⚠️ Failed to send OWNERSHIP_RELEASE to old session - continuing anyway`);
|
|
5221
|
+
}
|
|
5222
|
+
try {
|
|
5223
|
+
existingSession.disconnect();
|
|
5224
|
+
} catch (error) {
|
|
5225
|
+
this.logger.warn({ error, sessionId }, `⚠️ Failed to disconnect old session - continuing anyway`);
|
|
5226
|
+
}
|
|
5227
|
+
this.activeSessions.delete(sessionId);
|
|
5228
|
+
this.activeSessionsByUserId.delete(userId);
|
|
5229
|
+
this.logger.info({ sessionId, userId }, `✅ Old session cleaned up, proceeding with new connection`);
|
|
5230
|
+
}
|
|
5049
5231
|
const session = new AppSession({
|
|
5050
5232
|
packageName: this.config.packageName,
|
|
5051
5233
|
apiKey: this.config.apiKey,
|
|
@@ -5054,25 +5236,48 @@ class AppServer {
|
|
|
5054
5236
|
userId
|
|
5055
5237
|
});
|
|
5056
5238
|
const cleanupDisconnect = session.events.onDisconnected((info) => {
|
|
5239
|
+
let isPermanent = false;
|
|
5240
|
+
let reason = "unknown";
|
|
5057
5241
|
if (typeof info === "string") {
|
|
5058
5242
|
this.logger.info(`\uD83D\uDC4B Session ${sessionId} disconnected: ${info}`);
|
|
5243
|
+
reason = info;
|
|
5244
|
+
isPermanent = false;
|
|
5059
5245
|
} else {
|
|
5060
5246
|
this.logger.info(`\uD83D\uDC4B Session ${sessionId} disconnected: ${info.message} (code: ${info.code}, reason: ${info.reason})`);
|
|
5247
|
+
reason = info.reason || info.message;
|
|
5061
5248
|
if (info.sessionEnded === true) {
|
|
5062
5249
|
this.logger.info(`\uD83D\uDED1 User session ended for session ${sessionId}, calling onStop`);
|
|
5250
|
+
isPermanent = true;
|
|
5063
5251
|
this.onStop(sessionId, userId, "User session ended").catch((error) => {
|
|
5064
5252
|
this.logger.error(error, `❌ Error in onStop handler for session end:`);
|
|
5065
5253
|
});
|
|
5066
5254
|
} else if (info.permanent === true) {
|
|
5067
5255
|
this.logger.info(`\uD83D\uDED1 Permanent disconnection detected for session ${sessionId}, calling onStop`);
|
|
5068
|
-
|
|
5256
|
+
isPermanent = true;
|
|
5069
5257
|
this.onStop(sessionId, userId, `Connection permanently lost: ${info.reason}`).catch((error) => {
|
|
5070
5258
|
this.logger.error(error, `❌ Error in onStop handler for permanent disconnection:`);
|
|
5071
5259
|
});
|
|
5260
|
+
} else if (info.wasClean === true || info.code === 1000 || info.code === 1001) {
|
|
5261
|
+
this.logger.info(`\uD83D\uDED1 Clean WebSocket closure for session ${sessionId} (code: ${info.code}), treating as permanent`);
|
|
5262
|
+
isPermanent = true;
|
|
5263
|
+
this.onStop(sessionId, userId, `Clean disconnect: ${reason}`).catch((error) => {
|
|
5264
|
+
this.logger.error(error, `❌ Error in onStop handler for clean disconnect:`);
|
|
5265
|
+
});
|
|
5072
5266
|
}
|
|
5073
5267
|
}
|
|
5074
|
-
|
|
5075
|
-
|
|
5268
|
+
if (isPermanent) {
|
|
5269
|
+
if (this.activeSessions.get(sessionId) === session) {
|
|
5270
|
+
this.activeSessions.delete(sessionId);
|
|
5271
|
+
} else {
|
|
5272
|
+
this.logger.debug({ sessionId }, `\uD83D\uDD04 Session ${sessionId} cleanup skipped - a newer session has taken over`);
|
|
5273
|
+
}
|
|
5274
|
+
if (this.activeSessionsByUserId.get(userId) === session) {
|
|
5275
|
+
this.activeSessionsByUserId.delete(userId);
|
|
5276
|
+
}
|
|
5277
|
+
this.cleanupPhotoRequestsForSession(sessionId);
|
|
5278
|
+
} else {
|
|
5279
|
+
this.logger.debug({ sessionId, reason }, `\uD83D\uDD04 Temporary disconnect for session ${sessionId}, keeping in maps for reconnection`);
|
|
5280
|
+
}
|
|
5076
5281
|
});
|
|
5077
5282
|
const cleanupError = session.events.onError((error) => {
|
|
5078
5283
|
this.logger.error(error, `❌ [Session ${sessionId}] Error:`);
|
|
@@ -5175,10 +5380,20 @@ class AppServer {
|
|
|
5175
5380
|
process.on("SIGTERM", () => this.stop());
|
|
5176
5381
|
process.on("SIGINT", () => this.stop());
|
|
5177
5382
|
}
|
|
5178
|
-
cleanup() {
|
|
5383
|
+
async cleanup() {
|
|
5384
|
+
this.logger.info(`\uD83D\uDD27 [LOCAL SDK] cleanup() called - NOT sending OWNERSHIP_RELEASE`);
|
|
5179
5385
|
for (const [sessionId, session] of this.activeSessions) {
|
|
5180
|
-
this.logger.info(`\uD83D\uDC4B Closing session ${sessionId}`);
|
|
5181
|
-
|
|
5386
|
+
this.logger.info(`\uD83D\uDC4B Closing session ${sessionId} (no ownership release - cloud will resurrect)`);
|
|
5387
|
+
try {
|
|
5388
|
+
await session.disconnect({
|
|
5389
|
+
releaseOwnership: false
|
|
5390
|
+
});
|
|
5391
|
+
} catch (error) {
|
|
5392
|
+
this.logger.error(error, `Error during cleanup of session ${sessionId}`);
|
|
5393
|
+
try {
|
|
5394
|
+
await session.disconnect();
|
|
5395
|
+
} catch {}
|
|
5396
|
+
}
|
|
5182
5397
|
}
|
|
5183
5398
|
this.activeSessions.clear();
|
|
5184
5399
|
this.activeSessionsByUserId.clear();
|
|
@@ -5203,7 +5418,6 @@ class AppServer {
|
|
|
5203
5418
|
try {
|
|
5204
5419
|
const { requestId, type, success, errorCode, errorMessage } = req.body;
|
|
5205
5420
|
const photoFile = req.file;
|
|
5206
|
-
console.log("Received photo response: ", req.body);
|
|
5207
5421
|
this.logger.info({ requestId, type, success, errorCode }, `\uD83D\uDCF8 Received photo response: ${requestId} (type: ${type})`);
|
|
5208
5422
|
if (!requestId) {
|
|
5209
5423
|
this.logger.error("No requestId in photo response");
|
|
@@ -5212,24 +5426,18 @@ class AppServer {
|
|
|
5212
5426
|
error: "No requestId provided"
|
|
5213
5427
|
});
|
|
5214
5428
|
}
|
|
5215
|
-
const
|
|
5216
|
-
if (!
|
|
5217
|
-
this.logger.warn({ requestId }, "No
|
|
5429
|
+
const pending = this.completePhotoRequest(requestId);
|
|
5430
|
+
if (!pending) {
|
|
5431
|
+
this.logger.warn({ requestId, pendingCount: this.pendingPhotoRequests.size }, "\uD83D\uDCF8 No pending request found for photo (may have timed out or session ended)");
|
|
5218
5432
|
return res.status(404).json({
|
|
5219
5433
|
success: false,
|
|
5220
|
-
error: "No
|
|
5434
|
+
error: "No pending request found for this photo (may have timed out or session ended)"
|
|
5221
5435
|
});
|
|
5222
5436
|
}
|
|
5223
5437
|
if (type === "photo_error" || success === false) {
|
|
5224
|
-
const
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
error: {
|
|
5228
|
-
code: errorCode || "UNKNOWN_ERROR",
|
|
5229
|
-
message: errorMessage || "Unknown error occurred"
|
|
5230
|
-
}
|
|
5231
|
-
};
|
|
5232
|
-
session.camera.handlePhotoError(errorResponse);
|
|
5438
|
+
const errorMsg = errorMessage || "Unknown error occurred";
|
|
5439
|
+
this.logger.info({ requestId, errorCode, errorMessage: errorMsg }, "\uD83D\uDCF8 Photo error received");
|
|
5440
|
+
pending.reject(new Error(`Photo capture failed: ${errorMsg} (code: ${errorCode || "UNKNOWN_ERROR"})`));
|
|
5233
5441
|
return res.json({
|
|
5234
5442
|
success: true,
|
|
5235
5443
|
requestId,
|
|
@@ -5238,6 +5446,7 @@ class AppServer {
|
|
|
5238
5446
|
}
|
|
5239
5447
|
if (!photoFile) {
|
|
5240
5448
|
this.logger.error({ requestId }, "No photo file in successful upload");
|
|
5449
|
+
pending.reject(new Error("No photo file provided for successful upload"));
|
|
5241
5450
|
return res.status(400).json({
|
|
5242
5451
|
success: false,
|
|
5243
5452
|
error: "No photo file provided for successful upload"
|
|
@@ -5251,7 +5460,8 @@ class AppServer {
|
|
|
5251
5460
|
size: photoFile.size,
|
|
5252
5461
|
timestamp: new Date
|
|
5253
5462
|
};
|
|
5254
|
-
|
|
5463
|
+
this.logger.info({ requestId, size: photoFile.size, mimeType: photoFile.mimetype }, "\uD83D\uDCF8 Photo received successfully, resolving promise");
|
|
5464
|
+
pending.resolve(photoData);
|
|
5255
5465
|
res.json({
|
|
5256
5466
|
success: true,
|
|
5257
5467
|
requestId,
|
|
@@ -5266,6 +5476,53 @@ class AppServer {
|
|
|
5266
5476
|
}
|
|
5267
5477
|
});
|
|
5268
5478
|
}
|
|
5479
|
+
registerPhotoRequest(requestId, request) {
|
|
5480
|
+
const timeoutMs = 30000;
|
|
5481
|
+
const timeoutId = setTimeout(() => {
|
|
5482
|
+
const pending = this.pendingPhotoRequests.get(requestId);
|
|
5483
|
+
if (pending) {
|
|
5484
|
+
pending.reject(new Error("Photo request timed out"));
|
|
5485
|
+
this.pendingPhotoRequests.delete(requestId);
|
|
5486
|
+
this.logger.warn({ requestId }, "\uD83D\uDCF8 Photo request timed out");
|
|
5487
|
+
}
|
|
5488
|
+
}, timeoutMs);
|
|
5489
|
+
this.pendingPhotoRequests.set(requestId, {
|
|
5490
|
+
...request,
|
|
5491
|
+
timeoutId
|
|
5492
|
+
});
|
|
5493
|
+
this.logger.debug({ requestId, userId: request.userId, sessionId: request.sessionId }, "\uD83D\uDCF8 Photo request registered at AppServer level");
|
|
5494
|
+
}
|
|
5495
|
+
getPhotoRequest(requestId) {
|
|
5496
|
+
return this.pendingPhotoRequests.get(requestId);
|
|
5497
|
+
}
|
|
5498
|
+
completePhotoRequest(requestId) {
|
|
5499
|
+
const pending = this.pendingPhotoRequests.get(requestId);
|
|
5500
|
+
if (pending) {
|
|
5501
|
+
if (pending.timeoutId) {
|
|
5502
|
+
clearTimeout(pending.timeoutId);
|
|
5503
|
+
}
|
|
5504
|
+
this.pendingPhotoRequests.delete(requestId);
|
|
5505
|
+
this.logger.debug({ requestId }, "\uD83D\uDCF8 Photo request completed");
|
|
5506
|
+
}
|
|
5507
|
+
return pending;
|
|
5508
|
+
}
|
|
5509
|
+
cleanupPhotoRequestsForSession(sessionId) {
|
|
5510
|
+
let cleanedCount = 0;
|
|
5511
|
+
for (const [requestId, pending] of this.pendingPhotoRequests) {
|
|
5512
|
+
if (pending.sessionId === sessionId) {
|
|
5513
|
+
if (pending.timeoutId) {
|
|
5514
|
+
clearTimeout(pending.timeoutId);
|
|
5515
|
+
}
|
|
5516
|
+
pending.reject(new Error("Session ended"));
|
|
5517
|
+
this.pendingPhotoRequests.delete(requestId);
|
|
5518
|
+
cleanedCount++;
|
|
5519
|
+
this.logger.debug({ requestId, sessionId }, "\uD83D\uDCF8 Photo request cleaned up (session ended)");
|
|
5520
|
+
}
|
|
5521
|
+
}
|
|
5522
|
+
if (cleanedCount > 0) {
|
|
5523
|
+
this.logger.info({ sessionId, cleanedCount }, "\uD83D\uDCF8 Cleaned up photo requests for ended session");
|
|
5524
|
+
}
|
|
5525
|
+
}
|
|
5269
5526
|
setupMentraAuthRedirect() {
|
|
5270
5527
|
this.app.get("/mentra-auth", (req, res) => {
|
|
5271
5528
|
const authUrl = `https://account.mentra.glass/auth?packagename=${encodeURIComponent(this.config.packageName)}`;
|
|
@@ -5273,14 +5530,6 @@ class AppServer {
|
|
|
5273
5530
|
res.redirect(302, authUrl);
|
|
5274
5531
|
});
|
|
5275
5532
|
}
|
|
5276
|
-
findSessionByPhotoRequestId(requestId) {
|
|
5277
|
-
for (const [_sessionId, session] of this.activeSessions) {
|
|
5278
|
-
if (session.camera.hasPhotoPendingRequest(requestId)) {
|
|
5279
|
-
return session;
|
|
5280
|
-
}
|
|
5281
|
-
}
|
|
5282
|
-
return;
|
|
5283
|
-
}
|
|
5284
5533
|
}
|
|
5285
5534
|
|
|
5286
5535
|
class TpaServer extends AppServer {
|
|
@@ -5301,6 +5550,8 @@ export {
|
|
|
5301
5550
|
isValidLanguageCode,
|
|
5302
5551
|
isVad,
|
|
5303
5552
|
isUpdate,
|
|
5553
|
+
isUdpUnregister,
|
|
5554
|
+
isUdpRegister,
|
|
5304
5555
|
isStreamStatusCheckResponse,
|
|
5305
5556
|
isStreamCategory,
|
|
5306
5557
|
isStopWebhookRequest,
|
|
@@ -5332,6 +5583,7 @@ export {
|
|
|
5332
5583
|
isPhoneNotificationDismissed,
|
|
5333
5584
|
isPhoneNotification,
|
|
5334
5585
|
isPhoneBatteryUpdate,
|
|
5586
|
+
isOwnershipRelease,
|
|
5335
5587
|
isMicrophoneStateChange,
|
|
5336
5588
|
isManagedStreamStopRequest,
|
|
5337
5589
|
isManagedStreamStatus,
|
|
@@ -5400,6 +5652,7 @@ export {
|
|
|
5400
5652
|
PhotoStage,
|
|
5401
5653
|
PhotoErrorCode,
|
|
5402
5654
|
PermissionType,
|
|
5655
|
+
Observable,
|
|
5403
5656
|
LedModule,
|
|
5404
5657
|
LayoutType,
|
|
5405
5658
|
LEGACY_PERMISSION_MAP,
|
|
@@ -5408,6 +5661,7 @@ export {
|
|
|
5408
5661
|
GlassesToCloudMessageType,
|
|
5409
5662
|
GIVE_APP_CONTROL_OF_TOOL_RESPONSE,
|
|
5410
5663
|
EventTypes,
|
|
5664
|
+
DeviceState,
|
|
5411
5665
|
DashboardMode,
|
|
5412
5666
|
DashboardMessageTypes,
|
|
5413
5667
|
ControlActionTypes,
|
|
@@ -5425,4 +5679,4 @@ export {
|
|
|
5425
5679
|
AnimationUtils
|
|
5426
5680
|
};
|
|
5427
5681
|
|
|
5428
|
-
//# debugId=
|
|
5682
|
+
//# debugId=82AABDD8D6AA68F464756E2164756E21
|