@phystack/device-simulator 6.3.0

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.
@@ -0,0 +1,525 @@
1
+ /* eslint-disable import/prefer-default-export */
2
+ import { createHash } from "crypto";
3
+ import { v4 as uuidv4 } from "uuid";
4
+ import { Socket } from "socket.io";
5
+ import { TwinCache } from "./twin-cache";
6
+ import { EventPayload, TwinResponse, TwinTypeEnum } from "./types";
7
+ import { simulatorLog } from "./logger";
8
+
9
+ // Methods unsupported in simulator
10
+ const UNSUPPORTED_METHODS = new Set([
11
+ "setWirelessNetwork",
12
+ "setLanNetwork",
13
+ "reboot",
14
+ "setEnv",
15
+ ]);
16
+
17
+ // Signal methods — logged but no-op in local mode
18
+ const SIGNAL_METHODS = new Set([
19
+ "sendEventSignal",
20
+ "sendSessionSignal",
21
+ "sendClientSignal",
22
+ ]);
23
+
24
+ export class MessageRouter {
25
+ private twinCache: TwinCache;
26
+
27
+ private twinSubscriptions: Map<string, Set<string>> = new Map();
28
+
29
+ private messageDeliveryTracker: Map<string, Set<string>> = new Map();
30
+
31
+ private readonly MAX_TRACKED_MESSAGES = 1000;
32
+
33
+ private io: any; // socket.io server instance
34
+
35
+ constructor(twinCache: TwinCache, io: any) {
36
+ this.twinCache = twinCache;
37
+ this.io = io;
38
+ }
39
+
40
+ async handleMessage(
41
+ socket: Socket,
42
+ callerTwinId: string,
43
+ payload: EventPayload,
44
+ callback?: Function,
45
+ ): Promise<any> {
46
+ const { method } = payload;
47
+ simulatorLog.dim(
48
+ `← ${callerTwinId} ${method || "(no method)"} ${payload.twinId ? `→ ${payload.twinId}` : ""}`,
49
+ );
50
+
51
+ if (!method) {
52
+ simulatorLog.warn(
53
+ `Received event without method from ${callerTwinId}, ignoring`,
54
+ );
55
+ if (callback)
56
+ callback({ status: "error", message: "No method specified" });
57
+ return undefined;
58
+ }
59
+
60
+ if (UNSUPPORTED_METHODS.has(method)) {
61
+ const result = {
62
+ status: "error",
63
+ message: `${method} is not supported in simulator mode`,
64
+ };
65
+ if (callback) callback(result);
66
+ return result;
67
+ }
68
+
69
+ if (SIGNAL_METHODS.has(method)) {
70
+ const signalData = payload.data || {};
71
+ simulatorLog.info(
72
+ `Signal [${method}] from ${callerTwinId}: ${JSON.stringify(signalData)}`,
73
+ );
74
+ const result = { status: "success" };
75
+ if (callback) callback(result);
76
+ return result;
77
+ }
78
+
79
+ switch (method) {
80
+ case "getDeviceStatus":
81
+ return this.respond(callback, this.handleGetDeviceStatus());
82
+
83
+ case "getInstance":
84
+ case "getDeviceInstance":
85
+ return this.respond(callback, this.handleGetDeviceInstance());
86
+
87
+ case "getTwinById":
88
+ return this.respond(callback, this.handleGetTwinById(payload));
89
+
90
+ case "ping":
91
+ return this.respond(callback, { status: "success", message: "pong" });
92
+
93
+ case "twinMessage":
94
+ this.deliverTwinMessageLocally(callerTwinId, payload, callback);
95
+ return undefined;
96
+
97
+ case "twinSubscribe":
98
+ if (payload.twinId) {
99
+ this.handleTwinSubscribe(socket, callerTwinId, payload.twinId);
100
+ simulatorLog.info(
101
+ `twinSubscribe: ${callerTwinId} now listening to ${payload.twinId}`,
102
+ );
103
+ }
104
+ if (callback) callback({ status: "success" });
105
+ return undefined;
106
+
107
+ case "twinUnsubscribe":
108
+ if (payload.twinId) {
109
+ this.handleTwinUnsubscribe(callerTwinId, payload.twinId);
110
+ }
111
+ if (callback) callback({ status: "success" });
112
+ return undefined;
113
+
114
+ case "createInstanceTwin":
115
+ return this.respond(callback, this.handleCreateInstanceTwin(payload));
116
+
117
+ case "createPeripheralTwin":
118
+ return this.respond(callback, this.handleCreatePeripheralTwin(payload));
119
+
120
+ case "deletePeripheralTwin":
121
+ return this.respond(callback, this.handleDeletePeripheralTwin(payload));
122
+
123
+ case "getPeripheralTwins":
124
+ return this.respond(callback, this.handleGetPeripheralTwins(payload));
125
+
126
+ case "reportScreenTwinProperties":
127
+ case "reportEdgeTwinProperties":
128
+ case "reportPeripheralTwinProperties":
129
+ return this.respond(
130
+ callback,
131
+ this.handleReportProperties(method, payload),
132
+ );
133
+
134
+ case "reportDeviceTwinProperties":
135
+ return this.respond(
136
+ callback,
137
+ this.handleReportDeviceProperties(payload),
138
+ );
139
+
140
+ case "reportDeviceJob":
141
+ simulatorLog.dim("Device job reported (local mode, no-op)");
142
+ return this.respond(callback, { status: "success" });
143
+
144
+ default:
145
+ simulatorLog.warn(`Unknown method: ${method}`);
146
+ return this.respond(callback, {
147
+ status: "error",
148
+ message: `Unknown method: ${method}`,
149
+ });
150
+ }
151
+ }
152
+
153
+ /** Handle incoming twinMessage for local delivery (used internally) */
154
+ handleIncomingTwinMessage(payload: EventPayload): void {
155
+ this.deliverTwinMessageLocally("", payload);
156
+ }
157
+
158
+ /** Handle twinUpdated for local delivery */
159
+ handleIncomingTwinUpdated(twin: TwinResponse): void {
160
+ this.twinCache.updateTwin(twin);
161
+ this.io.to(twin.id).emit("twinMessage", {
162
+ method: "twinUpdated",
163
+ twinId: twin.id,
164
+ data: twin,
165
+ });
166
+ }
167
+
168
+ private respond(callback: Function | undefined, result: any): any {
169
+ if (callback) callback(result);
170
+ return result;
171
+ }
172
+
173
+ private handleGetDeviceStatus(): any {
174
+ const deviceTwin = this.twinCache.getDeviceTwin();
175
+ const desired = deviceTwin?.properties?.desired || {};
176
+ const reported = deviceTwin?.properties?.reported || {};
177
+ return {
178
+ status: "success",
179
+ socketConnected: true,
180
+ socketAuthenticated: true,
181
+ twins: this.twinCache.getGroupedByType(),
182
+ deviceId: deviceTwin?.deviceId,
183
+ tenantId: deviceTwin?.tenantId,
184
+ displayName: desired.displayName || "Simulator",
185
+ spaceId: desired.spaceId || "",
186
+ gridEnv: desired.env || "development",
187
+ dataResidency: "DEV",
188
+ accessKey: desired.accessKey || "simulator-local-key",
189
+ deviceSerial: desired.deviceSerial || "SIM-LOCAL",
190
+ osVersion: reported.os?.osVersion || "Simulator",
191
+ deviceEnv: desired.env || "development",
192
+ ip: reported.ip || [{ interface: "lo", ipv4: "127.0.0.1", ipv6: "::1" }],
193
+ isConnected: "true",
194
+ provisioningCode: "",
195
+ };
196
+ }
197
+
198
+ private handleGetDeviceInstance(): any {
199
+ const deviceTwin = this.twinCache.getDeviceTwin();
200
+ if (deviceTwin) {
201
+ return { status: "success", twin: deviceTwin };
202
+ }
203
+ return { status: "error", message: "Device twin not found" };
204
+ }
205
+
206
+ private handleGetTwinById(payload: EventPayload): any {
207
+ const twinId = (payload.data as any)?.twinId;
208
+ if (!twinId) {
209
+ return { status: "error", message: "Twin ID is required" };
210
+ }
211
+ const twin = this.twinCache.getTwin(twinId);
212
+ if (twin) {
213
+ return { status: "success", twin };
214
+ }
215
+ return { status: "error", message: "Twin not found" };
216
+ }
217
+
218
+ private handleCreateInstanceTwin(payload: EventPayload): any {
219
+ const { type, desiredProperties, id } = payload.data || {};
220
+ const deviceTwin = this.twinCache.getDeviceTwin();
221
+ const twin = this.createLocalTwin(
222
+ type || TwinTypeEnum.Screen,
223
+ desiredProperties || {},
224
+ deviceTwin,
225
+ id,
226
+ );
227
+ this.twinCache.addTwin(twin);
228
+ this.io.emit("twinCreated", { data: twin });
229
+ simulatorLog.info(`Twin created: ${JSON.stringify(twin)}`);
230
+ return { status: "success", twin };
231
+ }
232
+
233
+ private handleCreatePeripheralTwin(payload: EventPayload): any {
234
+ const { instanceId, name, hardwareId, desiredProperties } =
235
+ payload.data || {};
236
+ const deviceTwin = this.twinCache.getDeviceTwin();
237
+ const twin: TwinResponse = {
238
+ id: uuidv4(),
239
+ deviceId: deviceTwin?.deviceId || uuidv4(),
240
+ tenantId: deviceTwin?.tenantId || uuidv4(),
241
+ type: TwinTypeEnum.Peripheral,
242
+ properties: {
243
+ desired: { ...desiredProperties, instanceId, name, hardwareId },
244
+ reported: {},
245
+ },
246
+ descriptors: { instanceId, name, hardwareId },
247
+ };
248
+ this.twinCache.addTwin(twin);
249
+ this.io.emit("twinCreated", { data: twin });
250
+ return { status: "success", twin };
251
+ }
252
+
253
+ private handleDeletePeripheralTwin(payload: EventPayload): any {
254
+ const twinId = (payload.data as any)?.twinId || payload.twinId;
255
+ if (!twinId) {
256
+ return { status: "error", message: "Twin ID is required" };
257
+ }
258
+ const twin = this.twinCache.getTwin(twinId);
259
+ if (!twin) {
260
+ return { status: "error", message: "Twin not found" };
261
+ }
262
+ this.twinCache.removeTwin(twinId);
263
+ this.io.emit("twinDeleted", { data: twin });
264
+ return { status: "success" };
265
+ }
266
+
267
+ private handleGetPeripheralTwins(payload: EventPayload): any {
268
+ const instanceId = (payload.data as any)?.instanceId;
269
+ let peripherals = this.twinCache.getTwinsByType(TwinTypeEnum.Peripheral);
270
+ if (instanceId) {
271
+ peripherals = peripherals.filter(
272
+ (t) => t.descriptors?.instanceId === instanceId,
273
+ );
274
+ }
275
+ return { status: "success", twins: peripherals };
276
+ }
277
+
278
+ private handleReportProperties(method: string, payload: EventPayload): any {
279
+ const twinId = payload.twinId;
280
+ const properties = payload.data as Record<string, any>;
281
+ if (!twinId) {
282
+ return { status: "error", message: "Twin ID is required" };
283
+ }
284
+ const twin = this.twinCache.getTwin(twinId);
285
+ if (!twin) {
286
+ return { status: "error", message: "Twin not found" };
287
+ }
288
+ if (properties) {
289
+ twin.properties.reported = { ...twin.properties.reported, ...properties };
290
+ this.twinCache.updateTwin(twin);
291
+ this.handleIncomingTwinUpdated(twin);
292
+ }
293
+ return { status: "success", twin };
294
+ }
295
+
296
+ private handleReportDeviceProperties(payload: EventPayload): any {
297
+ const properties =
298
+ (payload.data as any)?.properties || (payload.data as any)?.reported;
299
+ const deviceTwin = this.twinCache.getDeviceTwin();
300
+ if (deviceTwin && properties) {
301
+ deviceTwin.properties.reported = {
302
+ ...deviceTwin.properties.reported,
303
+ ...properties,
304
+ };
305
+ this.twinCache.updateTwin(deviceTwin);
306
+ simulatorLog.dim("Device twin properties reported");
307
+ this.handleIncomingTwinUpdated(deviceTwin);
308
+ }
309
+ return { status: "success" };
310
+ }
311
+
312
+ private handleTwinSubscribe(
313
+ socket: Socket,
314
+ subscriberTwinId: string,
315
+ targetTwinId: string,
316
+ ): void {
317
+ if (!this.twinSubscriptions.has(subscriberTwinId)) {
318
+ this.twinSubscriptions.set(subscriberTwinId, new Set());
319
+ }
320
+ this.twinSubscriptions.get(subscriberTwinId)!.add(targetTwinId);
321
+ socket.join(targetTwinId);
322
+ simulatorLog.dim(`Twin ${subscriberTwinId} subscribed to ${targetTwinId}`);
323
+ }
324
+
325
+ private handleTwinUnsubscribe(
326
+ subscriberTwinId: string,
327
+ targetTwinId: string,
328
+ ): void {
329
+ const subscriptions = this.twinSubscriptions.get(subscriberTwinId);
330
+ if (subscriptions) {
331
+ subscriptions.delete(targetTwinId);
332
+ if (subscriptions.size === 0) {
333
+ this.twinSubscriptions.delete(subscriberTwinId);
334
+ }
335
+ }
336
+ simulatorLog.dim(
337
+ `Twin ${subscriberTwinId} unsubscribed from ${targetTwinId}`,
338
+ );
339
+ }
340
+
341
+ private getSocketsFromARoom(roomName: string): any[] {
342
+ const sockets: any[] = [];
343
+ const socketIds = this.io.sockets?.adapter?.rooms?.get(roomName);
344
+ socketIds?.forEach((socketId: string) => {
345
+ const socket = this.io.sockets?.sockets?.get(socketId);
346
+ if (socket) sockets.push(socket);
347
+ });
348
+ return sockets;
349
+ }
350
+
351
+ private generateMessageId(payload: EventPayload): string {
352
+ const { twinId, sourceTwinId, method, data } = payload;
353
+ const timestamp = Math.floor(Date.now() / 3000) * 3; // Round to nearest 3 seconds
354
+
355
+ const dataString = data
356
+ ? JSON.stringify(data, (_, value) =>
357
+ Buffer.isBuffer(value) ? value.toString("base64") : value,
358
+ )
359
+ : "";
360
+
361
+ const hash = createHash("sha256")
362
+ .update(`${twinId}-${sourceTwinId}-${method}-${dataString}-${timestamp}`)
363
+ .digest("hex");
364
+
365
+ return hash;
366
+ }
367
+
368
+ private trackMessageDelivery(messageId: string, recipientId: string): void {
369
+ if (!this.messageDeliveryTracker.has(messageId)) {
370
+ this.messageDeliveryTracker.set(messageId, new Set());
371
+
372
+ if (this.messageDeliveryTracker.size > this.MAX_TRACKED_MESSAGES) {
373
+ const oldestKey = this.messageDeliveryTracker.keys().next().value;
374
+ if (oldestKey) {
375
+ this.messageDeliveryTracker.delete(oldestKey);
376
+ }
377
+ }
378
+ }
379
+
380
+ this.messageDeliveryTracker.get(messageId)!.add(recipientId);
381
+ }
382
+
383
+ private hasMessageBeenDelivered(
384
+ messageId: string,
385
+ recipientId: string,
386
+ ): boolean {
387
+ return (
388
+ this.messageDeliveryTracker.get(messageId)?.has(recipientId) || false
389
+ );
390
+ }
391
+
392
+ private handleTwinMessageFromSubscription(
393
+ subscriberTwinId: string,
394
+ payload: EventPayload,
395
+ callback?: Function,
396
+ ): void {
397
+ const { twinId } = payload;
398
+ const roomSockets = this.getSocketsFromARoom(subscriberTwinId);
399
+
400
+ roomSockets.forEach((roomSocket) => {
401
+ roomSocket.emit("twinMessage", payload, (response: any) => {
402
+ // Response broadcasting: notify other sockets in the target twin's room
403
+ if (twinId && payload.data?.type) {
404
+ roomSocket.to(twinId).emit(payload.data.type, response);
405
+ }
406
+ if (callback) callback(response);
407
+ });
408
+ });
409
+ }
410
+
411
+ private deliverTwinMessageLocally(
412
+ callerTwinId: string,
413
+ payload: EventPayload,
414
+ callback?: Function,
415
+ ): void {
416
+ const { twinId, sourceTwinId } = payload;
417
+ if (!twinId) {
418
+ simulatorLog.warn("twinMessage: no target twinId, dropping");
419
+ return;
420
+ }
421
+
422
+ const dataType = (payload.data as any)?.type || "(no type)";
423
+
424
+ // Detect WebRTC signaling messages
425
+ const webrtcMatch = dataType.match(/^(.+):(offer|answer|ice)$/);
426
+ if (webrtcMatch) {
427
+ const [, channelPrefix, signalType] = webrtcMatch;
428
+ const isData = channelPrefix.startsWith("dc-");
429
+ const kind = isData ? "DataChannel" : "MediaStream";
430
+ simulatorLog.info(
431
+ `⚡ WebRTC ${signalType} (${kind}): ${sourceTwinId || "?"} → ${twinId}`,
432
+ );
433
+ } else {
434
+ simulatorLog.info(
435
+ `twinMessage: ${sourceTwinId || "?"} → ${twinId} [${dataType}]`,
436
+ );
437
+ }
438
+
439
+ const messageId = this.generateMessageId(payload);
440
+
441
+ // Layer 1: Subscription-based delivery (with dedup)
442
+ this.twinSubscriptions.forEach((subscribedTwins, subscriberTwinId) => {
443
+ if (
444
+ subscribedTwins.has(twinId) &&
445
+ !this.hasMessageBeenDelivered(messageId, subscriberTwinId)
446
+ ) {
447
+ this.handleTwinMessageFromSubscription(
448
+ subscriberTwinId,
449
+ payload,
450
+ callback,
451
+ );
452
+ this.trackMessageDelivery(messageId, subscriberTwinId);
453
+ }
454
+ });
455
+
456
+ // Layer 2: Type-based direct routing for Edge and Screen twins
457
+ const deviceTwins = this.twinCache.getGroupedByType();
458
+
459
+ if (deviceTwins[TwinTypeEnum.Edge]) {
460
+ for (const edgeTwinId of deviceTwins[TwinTypeEnum.Edge]) {
461
+ const edgeTwin = this.twinCache.getTwin(edgeTwinId);
462
+ if (
463
+ edgeTwin &&
464
+ edgeTwin.id === twinId &&
465
+ !this.hasMessageBeenDelivered(messageId, edgeTwin.id)
466
+ ) {
467
+ this.io.to(edgeTwin.id).emit("twinMessage", payload, callback);
468
+ this.trackMessageDelivery(messageId, edgeTwin.id);
469
+ }
470
+ }
471
+ }
472
+
473
+ if (deviceTwins[TwinTypeEnum.Screen]) {
474
+ for (const screenTwinId of deviceTwins[TwinTypeEnum.Screen]) {
475
+ const screenTwin = this.twinCache.getTwin(screenTwinId);
476
+ if (
477
+ screenTwin &&
478
+ screenTwin.id === twinId &&
479
+ !this.hasMessageBeenDelivered(messageId, screenTwin.id)
480
+ ) {
481
+ this.io.to(screenTwin.id).emit("twinMessage", payload, callback);
482
+ this.trackMessageDelivery(messageId, screenTwin.id);
483
+ }
484
+ }
485
+ }
486
+
487
+ // Layer 3: Peripheral → parent Edge routing
488
+ if (
489
+ deviceTwins[TwinTypeEnum.Peripheral] &&
490
+ deviceTwins[TwinTypeEnum.Edge]
491
+ ) {
492
+ for (const peripheralTwinId of deviceTwins[TwinTypeEnum.Peripheral]) {
493
+ const peripheralTwin = this.twinCache.getTwin(peripheralTwinId);
494
+ const instanceId = peripheralTwin?.properties?.desired?.instanceId;
495
+
496
+ if (
497
+ instanceId &&
498
+ deviceTwins[TwinTypeEnum.Edge].includes(instanceId) &&
499
+ !this.hasMessageBeenDelivered(messageId, instanceId)
500
+ ) {
501
+ this.io.to(instanceId).emit("twinMessage", payload);
502
+ this.trackMessageDelivery(messageId, instanceId);
503
+ }
504
+ }
505
+ }
506
+ }
507
+
508
+ private createLocalTwin(
509
+ type: TwinTypeEnum.Screen | TwinTypeEnum.Edge,
510
+ desiredProperties: Record<string, any>,
511
+ deviceTwin?: TwinResponse,
512
+ reuseId?: string,
513
+ ): TwinResponse {
514
+ return {
515
+ id: reuseId || uuidv4(),
516
+ deviceId: deviceTwin?.deviceId || uuidv4(),
517
+ tenantId: deviceTwin?.tenantId || uuidv4(),
518
+ type,
519
+ properties: {
520
+ desired: desiredProperties,
521
+ reported: {},
522
+ },
523
+ };
524
+ }
525
+ }
@@ -0,0 +1,61 @@
1
+ /* eslint-disable import/prefer-default-export */
2
+ import { TwinResponse, TwinTypeEnum } from "./types";
3
+
4
+ export class TwinCache {
5
+ private twins: Map<string, TwinResponse> = new Map();
6
+
7
+ populate(twins: TwinResponse[]): void {
8
+ for (const twin of twins) {
9
+ this.twins.set(twin.id, twin);
10
+ }
11
+ }
12
+
13
+ getTwin(id: string): TwinResponse | undefined {
14
+ return this.twins.get(id);
15
+ }
16
+
17
+ getDeviceTwin(): TwinResponse | undefined {
18
+ for (const twin of this.twins.values()) {
19
+ if (twin.type === TwinTypeEnum.Device) return twin;
20
+ }
21
+ return undefined;
22
+ }
23
+
24
+ getAllTwins(): TwinResponse[] {
25
+ return Array.from(this.twins.values());
26
+ }
27
+
28
+ getAllTwinIds(): string[] {
29
+ return Array.from(this.twins.keys());
30
+ }
31
+
32
+ hasTwin(id: string): boolean {
33
+ return this.twins.has(id);
34
+ }
35
+
36
+ updateTwin(twin: TwinResponse): void {
37
+ this.twins.set(twin.id, twin);
38
+ }
39
+
40
+ addTwin(twin: TwinResponse): void {
41
+ this.twins.set(twin.id, twin);
42
+ }
43
+
44
+ removeTwin(id: string): void {
45
+ this.twins.delete(id);
46
+ }
47
+
48
+ getTwinsByType(type: string): TwinResponse[] {
49
+ return Array.from(this.twins.values()).filter((t) => t.type === type);
50
+ }
51
+
52
+ /** Returns twins grouped by type as { [type]: [twinId, ...] } — matching hub-device getCachedTwins format */
53
+ getGroupedByType(): Record<string, string[]> {
54
+ const grouped: Record<string, string[]> = {};
55
+ for (const twin of this.twins.values()) {
56
+ if (!grouped[twin.type]) grouped[twin.type] = [];
57
+ grouped[twin.type].push(twin.id);
58
+ }
59
+ return grouped;
60
+ }
61
+ }
@@ -0,0 +1,53 @@
1
+ export interface SimulatorConfig {
2
+ port: number;
3
+ deviceDisplayName?: string;
4
+ }
5
+
6
+ export interface DeviceConfig {
7
+ deviceId: string;
8
+ tenantId: string;
9
+ deviceTwinId: string;
10
+ }
11
+
12
+ export enum AppTypeEnum {
13
+ Screen = "screen",
14
+ Edge = "edge",
15
+ }
16
+
17
+ export interface AppConfig {
18
+ name: string;
19
+ type: AppTypeEnum;
20
+ path: string;
21
+ twinId?: string;
22
+ devCommand: string;
23
+ }
24
+
25
+ export enum TwinTypeEnum {
26
+ Device = "Device",
27
+ Screen = "Screen",
28
+ Edge = "Edge",
29
+ Peripheral = "Peripheral",
30
+ }
31
+
32
+ export interface TwinResponse {
33
+ id: string;
34
+ deviceId: string;
35
+ tenantId: string;
36
+ type: TwinTypeEnum;
37
+ properties: {
38
+ desired: Record<string, any>;
39
+ reported: Record<string, any>;
40
+ };
41
+ descriptors?: Record<string, any>;
42
+ status?: string;
43
+ }
44
+
45
+ export interface EventPayload<T = any> {
46
+ twinId?: string;
47
+ deviceId?: string;
48
+ sourceTwinId?: string;
49
+ sourceDeviceId?: string;
50
+ method?: string;
51
+ data?: T;
52
+ [key: string]: any;
53
+ }