@alwaysai/device-agent 0.0.6 → 0.0.8

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.
Files changed (36) hide show
  1. package/lib/application-control/environment-variables.js +2 -2
  2. package/lib/application-control/environment-variables.js.map +1 -1
  3. package/lib/application-control/install.d.ts +14 -10
  4. package/lib/application-control/install.d.ts.map +1 -1
  5. package/lib/application-control/install.js +44 -44
  6. package/lib/application-control/install.js.map +1 -1
  7. package/lib/application-control/models.d.ts.map +1 -1
  8. package/lib/application-control/models.js +4 -1
  9. package/lib/application-control/models.js.map +1 -1
  10. package/lib/application-control/status.d.ts +3 -2
  11. package/lib/application-control/status.d.ts.map +1 -1
  12. package/lib/application-control/status.js +8 -6
  13. package/lib/application-control/status.js.map +1 -1
  14. package/lib/application-control/utils.d.ts +4 -0
  15. package/lib/application-control/utils.d.ts.map +1 -1
  16. package/lib/application-control/utils.js +20 -6
  17. package/lib/application-control/utils.js.map +1 -1
  18. package/lib/cloud-connection/device-agent-cloud-connection.d.ts +35 -15
  19. package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
  20. package/lib/cloud-connection/device-agent-cloud-connection.js +282 -196
  21. package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
  22. package/lib/subcommands/app/app.js +1 -1
  23. package/lib/subcommands/app/app.js.map +1 -1
  24. package/lib/util/sleep.d.ts +2 -0
  25. package/lib/util/sleep.d.ts.map +1 -0
  26. package/lib/util/sleep.js +9 -0
  27. package/lib/util/sleep.js.map +1 -0
  28. package/package.json +4 -3
  29. package/src/application-control/environment-variables.ts +2 -2
  30. package/src/application-control/install.ts +66 -56
  31. package/src/application-control/models.ts +4 -1
  32. package/src/application-control/status.ts +19 -9
  33. package/src/application-control/utils.ts +25 -5
  34. package/src/cloud-connection/device-agent-cloud-connection.ts +374 -223
  35. package/src/subcommands/app/app.ts +1 -1
  36. package/src/util/sleep.ts +5 -0
@@ -5,178 +5,162 @@ import {
5
5
  getCertificateFilePath,
6
6
  getRootCertificateFilePath,
7
7
  } from '../util/directories';
8
+ import sleep from '../util/sleep';
8
9
  import {
9
10
  startApp,
10
11
  stopApp,
11
12
  restartApp,
12
- getAppStatus,
13
13
  getAppLogs,
14
+ getAppStatus,
14
15
  } from '../application-control/status';
15
- import { installApp } from '../application-control/install';
16
- import { getCpuUtil, getDiskUtil, getMemUtil } from '../device-control/device-control';
16
+ import { installApp, uninstallApp } from '../application-control/install';
17
17
  import {
18
- DeviceAgentMessage,
18
+ keyMirrors,
19
+ validateClientMessage,
20
+ DeviceAgentMessagePayload,
21
+ AppInstallStatusPacket,
22
+ AppStateControlPacket,
23
+ AppVersionControlPacket,
24
+ LiveUpdatesToggles,
25
+ AppLogsPacket,
26
+ AppStatePacket,
27
+ LiveUpdatesToggleMessage,
19
28
  DeviceStatsMessage,
20
- AppState,
21
29
  AppStateMessage,
22
- ActionMessage,
23
- AppLogs,
24
30
  AppLogsMessage,
25
- InstallationStatusMessage,
26
- InstallationStatus,
27
- InstallationStatusEnum,
31
+ AppInstallStatusMessage,
32
+ AppInstallSignedUrlsRequestMessage,
33
+ DeviceAgentMessage,
34
+ ClientMessage,
28
35
  } from '@alwaysai/device-agent-schemas';
29
36
  import { getDeviceId } from '../util/get-device-id';
37
+ import { logger } from 'alwaysai/lib/util';
38
+ import { getCpuUtil, getDiskUtil, getMemUtil } from '../device-control/device-control';
30
39
  import { AgentConfigFile } from '../infrastructure/agent-config';
31
40
 
32
41
  export class DeviceAgentCloudConnection {
33
42
  private clientId = getDeviceId();
34
43
  private host = getIoTCoreEndpointUrl();
35
- private publishable = false;
36
- private readonly publishInterval = 10000;
37
- private readonly installationStatusInterval = 5000;
38
- private installationStatus: InstallationStatus;
39
- private publishableTimeout: ReturnType<typeof setTimeout>;
40
-
41
- public device = awsIot.device;
42
- public agentTopicPrefix = `destination/agent/device/${this.clientId}/topic/`;
43
- public cloudTopicPrefix = `destination/cloud/device/${this.clientId}/topic/`;
44
+ private appInstallStatus: AppInstallStatusPacket;
45
+ private liveUpdatesTimeout: ReturnType<typeof setTimeout>;
46
+ private liveUpdatesAlive = {
47
+ [keyMirrors.agentMessageType.device_stats]: false,
48
+ [keyMirrors.agentMessageType.app_state]: false,
49
+ [keyMirrors.agentMessageType.app_logs]: false,
50
+ };
51
+ private liveUpdatesSleepIntervals = {
52
+ [keyMirrors.agentMessageType.device_stats]: 5000,
53
+ [keyMirrors.agentMessageType.app_state]: 5000,
54
+ [keyMirrors.agentMessageType.app_logs]: 5000,
55
+ [keyMirrors.agentMessageType.app_install_status]: 5000,
56
+ };
57
+ private appLogStreams = new Set<string>();
58
+ private readonly agentTopicPrefix = `destination/agent/device/${this.clientId}/topic/`;
59
+ private readonly cloudTopicPrefix = `destination/cloud/device/${this.clientId}/topic/`;
60
+ private readonly publishableTopics = {
61
+ deviceStats: `${this.cloudTopicPrefix}device-management`,
62
+ appState: `${this.cloudTopicPrefix}application-management`,
63
+ appLogs: `${this.cloudTopicPrefix}application-management`,
64
+ appInstallStatus: `${this.cloudTopicPrefix}installation-status`,
65
+ cloudRequest: `${this.cloudTopicPrefix}request`,
66
+ };
67
+ private readonly subsribedTopics = {
68
+ command: `${this.agentTopicPrefix}command`,
69
+ response: `${this.agentTopicPrefix}response`,
70
+ };
44
71
 
45
- constructor() {
46
- this.device = awsIot.device({
47
- keyPath: getPrivateKeyFilePath(),
48
- certPath: getCertificateFilePath(),
49
- caPath: getRootCertificateFilePath(),
50
- clientId: this.clientId,
51
- host: this.host,
72
+ public async startAppLogStream(projectId: string) {
73
+ this.appLogStreams.add(projectId);
74
+ const readable = await getAppLogs({
75
+ projectId,
76
+ args: ['--tail', '100', '--no-log-prefix'],
77
+ });
78
+ readable.on('data', (chunk: Buffer) => {
79
+ if (!this.appLogStreams.has(projectId)) {
80
+ // why doesn't typescript know about this function?
81
+ // @ts-ignore
82
+ readable.destroy();
83
+ logger.info(`App log stream terminated for project ${projectId}`);
84
+ return;
85
+ }
86
+ const logStr = chunk.toString();
87
+ const message = {
88
+ messageType: keyMirrors.agentMessageType.app_logs,
89
+ appLogs: {
90
+ projectId,
91
+ logChunk: logStr,
92
+ },
93
+ };
94
+ const packet = this.buildMessagePacket(
95
+ this.getClientId(),
96
+ this.publishableTopics.appLogs,
97
+ message,
98
+ );
99
+ this.publishMessage(this.publishableTopics.appLogs, JSON.stringify(packet));
52
100
  });
53
101
 
54
- this.device.subscribe(`${this.agentTopicPrefix}command`);
55
- this.device.subscribe(`${this.agentTopicPrefix}response`);
56
- }
57
-
58
- public setInstallationStatus(installationStatus: InstallationStatus) {
59
- this.installationStatus = installationStatus;
60
- }
61
-
62
- public getInstallationStatus(): InstallationStatus {
63
- return this.installationStatus;
64
- }
65
-
66
- public getPublishable(): boolean {
67
- return this.publishable;
68
- }
69
-
70
- public setPublishable(enabled: boolean) {
71
- this.publishable = enabled;
72
- }
102
+ readable.on('error', (error) => {
103
+ logger.error(`App log stream terminated for project ${projectId}: ${error}`);
104
+ });
73
105
 
74
- public restartPublishableTimeout() {
75
- clearTimeout(this.publishableTimeout);
76
- this.publishableTimeout = setTimeout(() => {
77
- this.setPublishable(false);
78
- }, 600000); // 10 min
106
+ readable.on('finished', () => {
107
+ console.log(`App logs finished piping for project ${projectId}`);
108
+ });
79
109
  }
80
110
 
81
- public getPublishInterval() {
82
- return this.publishInterval;
111
+ // must contain app release hash
112
+ private initAppInstallStatus(installationStatus: AppInstallStatusPacket) {
113
+ this.appInstallStatus = installationStatus;
83
114
  }
84
115
 
85
- public getInstallationStatusInterval() {
86
- return this.installationStatusInterval;
116
+ private updateAppInstallStatus(
117
+ installationStatus: Omit<AppInstallStatusPacket, 'appReleaseHash'>,
118
+ ) {
119
+ this.appInstallStatus.status = installationStatus.status;
120
+ this.appInstallStatus.message = installationStatus.message;
87
121
  }
88
122
 
89
- public getClientId(): string {
90
- return this.clientId;
123
+ private getAppInstallStatus(): AppInstallStatusPacket {
124
+ return this.appInstallStatus;
91
125
  }
92
126
 
93
- public publishMessage(topic: string, message: string) {
94
- this.device.publish(topic, message);
95
- }
96
- }
97
-
98
- export function runDeviceAgentCloudInterface() {
99
- const deviceAgent = new DeviceAgentCloudConnection();
100
-
101
- async function buildMessagePacket(
127
+ // Message Builders
128
+ private buildMessagePacket(
129
+ deviceId: string,
102
130
  topic: string,
103
- payload:
104
- | DeviceStatsMessage
105
- | ActionMessage
106
- | AppStateMessage
107
- | AppLogsMessage
108
- | InstallationStatusMessage,
109
- ): Promise<DeviceAgentMessage> {
131
+ payload: DeviceAgentMessagePayload,
132
+ ): DeviceAgentMessage {
110
133
  const packet = {
111
134
  timestamp: new Date().toUTCString(),
112
- deviceId: deviceAgent.getClientId(),
135
+ deviceId,
113
136
  topic,
114
137
  payload,
115
138
  };
116
139
  return packet;
117
140
  }
118
141
 
119
- async function getAppLogsMessage(): Promise<AppLogsMessage> {
120
- const appLogsList: AppLogs[] = [];
142
+ private async getAppStateMessage(): Promise<AppStateMessage> {
143
+ const appStateMessage: AppStatePacket[] = [];
121
144
  const apps = await AgentConfigFile().getReadyApps();
122
- for (const app of apps) {
123
- const projectId = app.projectId;
124
- const readable = await getAppLogs({ projectId });
125
- const logs: any[] = [];
126
- readable.setEncoding('utf8');
127
- for await (const chunk of readable) {
128
- logs.push(chunk);
129
- }
130
- const appLogs = {
131
- projectId,
132
- logs,
133
- };
134
- appLogsList.push(appLogs);
135
- }
136
- const applicationStatePackage = {
137
- applicationLogs: appLogsList,
138
- };
139
- return applicationStatePackage;
140
- }
141
-
142
- // * This was causing massive CPU overhead
143
- // const publishAppLogs = setInterval(async function () {
144
- // const appLogsMessage = await getAppLogsMessage();
145
- // const topic = `device/${deviceAgent.getClientId()}/topic/application-management`;
146
- // const appLogsPacket = await buildMessagePacket(topic, appLogsMessage);
147
-
148
- // deviceAgent.publishMessage(topic, JSON.stringify({ appLogsPacket }));
149
- // }, deviceAgent.getPublishInterval());
150
-
151
- async function getAppStateMessage(): Promise<AppStateMessage> {
152
- const appStateMessage: AppState[] = [];
153
- const apps = await AgentConfigFile().getApps();
154
145
  for (const app of apps) {
155
146
  const projectId = app.projectId;
156
147
  const status = await getAppStatus({ projectId });
157
148
  appStateMessage.push(status);
158
149
  }
159
- const applicationStatePackage = {
160
- applicationState: appStateMessage,
150
+ const appStatePackage = {
151
+ messageType: keyMirrors.agentMessageType.app_state,
152
+ appState: appStateMessage,
161
153
  };
162
- return applicationStatePackage;
154
+ return appStatePackage;
163
155
  }
164
156
 
165
- const publishAppState = setInterval(async function () {
166
- if (deviceAgent.getPublishable()) {
167
- const topic = `${deviceAgent.cloudTopicPrefix}application-management`;
168
- const appStateMessage = await getAppStateMessage();
169
- const appStatePacket = await buildMessagePacket(topic, appStateMessage);
170
- deviceAgent.publishMessage(topic, JSON.stringify({ appStatePacket }));
171
- }
172
- }, deviceAgent.getPublishInterval());
173
-
174
- async function getDeviceStatsMessage(): Promise<DeviceStatsMessage> {
157
+ private async getDeviceStatsMessage(): Promise<DeviceStatsMessage> {
175
158
  const cpuUsage = await getCpuUtil();
176
159
  const diskUtil = await getDiskUtil();
177
160
  const memUtil = await getMemUtil();
178
161
 
179
162
  const deviceStatsMessage = {
163
+ messageType: keyMirrors.agentMessageType.device_stats,
180
164
  deviceStats: {
181
165
  cpuUsage,
182
166
  diskUtil,
@@ -186,124 +170,286 @@ export function runDeviceAgentCloudInterface() {
186
170
  return deviceStatsMessage;
187
171
  }
188
172
 
189
- const publishDeviceStats = setInterval(async function () {
190
- if (deviceAgent.getPublishable()) {
191
- const topic = `${deviceAgent.cloudTopicPrefix}device-management`;
192
- const deviceStatsMessage = await getDeviceStatsMessage();
193
- const deviceStatsPacket = await buildMessagePacket(topic, deviceStatsMessage);
194
- deviceAgent.publishMessage(topic, JSON.stringify({ deviceStatsPacket }));
195
- }
196
- }, deviceAgent.getPublishInterval());
197
-
198
- const publishDeviceRequest = async ({ projectId, releaseHash }) => {
199
- const topic = `${deviceAgent.cloudTopicPrefix}request`;
200
- const deviceRequestPackage = {
201
- timestamp: new Date().toUTCString(),
202
- deviceId: deviceAgent.getClientId(),
203
- projectId,
204
- releaseHash,
205
- topic,
173
+ // must be arrow function due to this context when function is passed as param
174
+ private getAppInstallStatusMessage = async (): Promise<AppInstallStatusMessage> => {
175
+ const appInstallStatus = this.getAppInstallStatus();
176
+ const appInstallStatusMessage = {
177
+ messageType: keyMirrors.agentMessageType.app_install_status,
178
+ appInstallStatus,
206
179
  };
207
- deviceAgent.publishMessage(
208
- topic,
209
- JSON.stringify({ device_request: deviceRequestPackage }),
210
- );
180
+ return appInstallStatusMessage;
211
181
  };
212
182
 
213
- const publishInstallationStatus = async (interval: NodeJS.Timeout) => {
214
- const topic = `${deviceAgent.cloudTopicPrefix}installation-status`;
215
- const installationStatus = deviceAgent.getInstallationStatus();
216
- const installationStatusPacket = await buildMessagePacket(topic, {
217
- installationStatus,
218
- });
219
- deviceAgent.device.publish(topic, JSON.stringify({ installationStatusPacket }));
220
- if (installationStatus.status !== InstallationStatusEnum.IN_PROGRESS) {
221
- clearInterval(interval);
183
+ private async startPublishingLiveUpdates(
184
+ topic: string,
185
+ messageType: string,
186
+ getMessageData: () => Promise<DeviceAgentMessagePayload>,
187
+ ) {
188
+ while (true) {
189
+ try {
190
+ const message = await getMessageData();
191
+ const packet = this.buildMessagePacket(this.getClientId(), topic, message);
192
+ this.publishMessage(topic, JSON.stringify(packet));
193
+ } catch (e) {
194
+ logger.error(`Error publishing live updates for ${messageType}: ${e.message}`);
195
+ break;
196
+ }
197
+ if (!this.continuePublishing(messageType)) {
198
+ logger.info(`Turned off live updates for ${messageType}`);
199
+ break;
200
+ }
201
+ await sleep(this.getLiveUpdatesInterval(messageType));
222
202
  }
223
- };
203
+ }
224
204
 
225
- const handleMessageTopic = async ({ topic, payload }) => {
226
- const action = payload['action'];
227
- const actionPayload: any = payload[action];
228
- const type = topic.split('/').slice(-1)[0];
205
+ private continuePublishing(flag: string): boolean {
206
+ switch (flag) {
207
+ case keyMirrors.agentMessageType.device_stats:
208
+ case keyMirrors.agentMessageType.app_state:
209
+ return this.liveUpdatesAlive[flag];
210
+ case keyMirrors.agentMessageType.app_install_status:
211
+ return this.appInstallStatus.status === keyMirrors.appInstallStatus.in_progress;
212
+ default:
213
+ logger.error(`Unrecognized publishable flag ${flag}`);
214
+ return false;
215
+ }
216
+ }
229
217
 
230
- if (!type) {
231
- return false;
218
+ private getLiveUpdatesInterval(flag: string): number {
219
+ const exists = this.liveUpdatesSleepIntervals[flag];
220
+ if (exists) {
221
+ return exists;
232
222
  }
223
+ logger.error(`Unrecognized live updates flag ${flag}`);
224
+ return -1;
225
+ }
233
226
 
234
- switch (action) {
235
- /**
236
- * Install app package based on the given project ID
237
- */
238
- case 'install':
239
- /**
240
- {
241
- "action": "install",
242
- "install": {
243
- "releaseHash": "7fb2a812f9e7aa193208dac353521965da50d755085162066c125592f1ed760b",
244
- "projectId": "786e4686-a681-4cff-9e17-1e7d385c0fdb"
245
- }
246
- }
247
- */
248
- if (type === 'response') {
249
- deviceAgent.setInstallationStatus({
250
- status: InstallationStatusEnum.IN_PROGRESS,
251
- applicationReleaseHash: actionPayload.releaseHash as string,
252
- });
227
+ private setLiveUpdates(toggles: LiveUpdatesToggles) {
228
+ if (toggles.deviceStats) {
229
+ this.liveUpdatesAlive.device_stats = toggles.deviceStats;
230
+ }
231
+ if (toggles.appState) {
232
+ this.liveUpdatesAlive.app_state = toggles.appState;
233
+ }
234
+ }
253
235
 
254
- const installationStatusPing = setInterval(
255
- () => publishInstallationStatus(installationStatusPing),
256
- deviceAgent.getInstallationStatusInterval(),
257
- );
258
-
259
- // Install the app with the given url
260
- await (async () => {
261
- try {
262
- await installApp(actionPayload);
263
- deviceAgent.setInstallationStatus({
264
- status: InstallationStatusEnum.SUCCESS,
265
- applicationReleaseHash: actionPayload.releaseHash as string,
266
- });
267
- } catch (e) {
268
- const reason: string = e.message;
269
- deviceAgent.setInstallationStatus({
270
- status: InstallationStatusEnum.FAILURE,
271
- reason,
272
- applicationReleaseHash: actionPayload.releaseHash as string,
273
- });
274
- }
275
- })();
276
- break;
236
+ private async handleCloudResponse(message: ClientMessage) {
237
+ const payload = message.payload;
238
+ switch (payload.messageType) {
239
+ case keyMirrors.clientMessageType.app_install_cloud_response: {
240
+ const { projectId, appReleaseHash, appInstallPayload, modelsInstallPayload } =
241
+ payload.appInstallCloudResponse;
242
+
243
+ this.initAppInstallStatus({
244
+ status: keyMirrors.appInstallStatus.in_progress,
245
+ appReleaseHash,
246
+ });
247
+
248
+ this.startPublishingLiveUpdates(
249
+ this.publishableTopics.appInstallStatus,
250
+ keyMirrors.agentMessageType.app_install_status,
251
+ this.getAppInstallStatusMessage,
252
+ );
253
+
254
+ // Install the app and models
255
+ try {
256
+ const signedUrlsPayload = {
257
+ appInstallPayload,
258
+ modelsInstallPayload,
259
+ };
260
+ await installApp({
261
+ projectId,
262
+ appReleaseHash,
263
+ signedUrlsPayload,
264
+ });
265
+ this.updateAppInstallStatus({
266
+ status: keyMirrors.appInstallStatus.success,
267
+ });
268
+ } catch (e) {
269
+ console.error(e);
270
+ const message: string = e.message;
271
+
272
+ // uninstall the failed app to put system back in good state
273
+ await uninstallApp({ projectId });
274
+ this.updateAppInstallStatus({
275
+ status: keyMirrors.appInstallStatus.failure,
276
+ message,
277
+ });
277
278
  }
278
- publishDeviceRequest(actionPayload);
279
- break;
280
- case 'start':
281
- startApp({ projectId: actionPayload.projectId });
282
279
  break;
283
- case 'stop':
284
- stopApp({ projectId: actionPayload.projectId });
280
+ }
281
+ default:
282
+ logger.error(`Invalid cloud response message type '${message}'`);
283
+ }
284
+ }
285
+
286
+ private handleClientMessage(message: ClientMessage) {
287
+ const payload = message.payload;
288
+ switch (payload.messageType) {
289
+ case keyMirrors.clientMessageType.app_state_control:
290
+ this.handleAppStateControl(payload.appStateControl);
285
291
  break;
286
- case 'restart':
287
- restartApp({ projectId: actionPayload.projectId });
292
+ case keyMirrors.clientMessageType.app_version_control:
293
+ this.handleAppVersionControl(payload.appVersionControl);
288
294
  break;
289
- /**
290
- * Allow/disallow publishing on this device. By default, (publishable = false)
291
- */
292
- case 'publishable':
293
- /**
294
- {
295
- "action": "publishable",
296
- "publishable": true | false
297
- }
298
- */
299
- deviceAgent.setPublishable(actionPayload);
300
- deviceAgent.restartPublishableTimeout();
295
+ case keyMirrors.clientMessageType.live_state_updates:
296
+ this.handleAgentCommand(payload);
301
297
  break;
302
298
  default:
299
+ logger.error(`Invalid Client Message '${JSON.stringify(payload)}'`);
300
+ }
301
+ }
302
+
303
+ private handleAppStateControl(payload: AppStateControlPacket) {
304
+ const { baseCommand, projectId } = payload;
305
+ switch (baseCommand) {
306
+ case keyMirrors.appStateControl.start:
307
+ startApp({ projectId });
308
+ break;
309
+ case keyMirrors.appStateControl.stop:
310
+ stopApp({ projectId });
311
+ break;
312
+ case keyMirrors.appStateControl.restart:
313
+ restartApp({ projectId });
303
314
  break;
304
315
  }
316
+ }
317
+
318
+ private handleAppVersionControl(payload: AppVersionControlPacket) {
319
+ const { projectId, appReleaseHash } = payload;
320
+ const signedUrlsRequest = { projectId, appReleaseHash };
321
+ this.publishCloudRequest({
322
+ messageType: keyMirrors.agentMessageType.app_install_signed_urls_request,
323
+ signedUrlsRequest,
324
+ });
325
+ }
326
+
327
+ private handleDeviceCommand = async (packet: any) => {
328
+ // TODO
305
329
  };
306
330
 
331
+ private handleAgentCommand(message: LiveUpdatesToggleMessage) {
332
+ switch (message.messageType) {
333
+ case keyMirrors.clientMessageType.live_state_updates:
334
+ this.liveUpdatesBroker(message.liveUpdatesToggles);
335
+ break;
336
+ default:
337
+ logger.error(`Invalid agent action message type from message '${message}'`);
338
+ }
339
+ }
340
+
341
+ private restartLiveUpdatesTimeout() {
342
+ clearTimeout(this.liveUpdatesTimeout);
343
+ this.liveUpdatesTimeout = setTimeout(() => {
344
+ this.setLiveUpdates({
345
+ deviceStats: false,
346
+ appState: false,
347
+ });
348
+ this.appLogStreams.clear();
349
+ // TODO: Make constant, not hard coded
350
+ }, 600000); // 10 min
351
+ }
352
+
353
+ private async liveUpdatesBroker({
354
+ deviceStats,
355
+ appState,
356
+ appLogs,
357
+ }: {
358
+ deviceStats?: boolean;
359
+ appState?: boolean;
360
+ appLogs?: {
361
+ projectId: string;
362
+ toggle: boolean;
363
+ };
364
+ }) {
365
+ this.restartLiveUpdatesTimeout();
366
+
367
+ if (deviceStats !== undefined) {
368
+ this.liveUpdatesAlive.device_stats = deviceStats;
369
+ if (deviceStats) {
370
+ this.startPublishingLiveUpdates(
371
+ this.publishableTopics.deviceStats,
372
+ keyMirrors.agentMessageType.device_stats,
373
+ this.getDeviceStatsMessage,
374
+ );
375
+ }
376
+ }
377
+
378
+ if (appState !== undefined) {
379
+ this.liveUpdatesAlive.app_state = appState;
380
+ if (appState) {
381
+ this.startPublishingLiveUpdates(
382
+ this.publishableTopics.appState,
383
+ keyMirrors.agentMessageType.app_state,
384
+ this.getAppStateMessage,
385
+ );
386
+ }
387
+ }
388
+
389
+ if (appLogs !== undefined) {
390
+ if (appLogs.toggle) {
391
+ this.startAppLogStream(appLogs.projectId);
392
+ } else {
393
+ this.appLogStreams.delete(appLogs.projectId);
394
+ }
395
+ }
396
+ }
397
+
398
+ private async publishCloudRequest(payload: AppInstallSignedUrlsRequestMessage) {
399
+ const topic = this.publishableTopics.cloudRequest;
400
+ const deviceRequestPacket = this.buildMessagePacket(
401
+ this.getClientId(),
402
+ topic,
403
+ payload,
404
+ );
405
+ this.publishMessage(topic, JSON.stringify({ deviceRequestPacket }));
406
+ }
407
+
408
+ // Public Methods
409
+
410
+ public device = awsIot.device;
411
+
412
+ constructor() {
413
+ this.device = awsIot.device({
414
+ keyPath: getPrivateKeyFilePath(),
415
+ certPath: getCertificateFilePath(),
416
+ caPath: getRootCertificateFilePath(),
417
+ clientId: this.clientId,
418
+ host: this.host,
419
+ });
420
+
421
+ Object.values(this.subsribedTopics).forEach((topic: string) => {
422
+ this.device.subscribe(topic);
423
+ });
424
+ }
425
+
426
+ public getClientId(): string {
427
+ return this.clientId;
428
+ }
429
+
430
+ public publishMessage(topic: string, message: string) {
431
+ // TODO: topic validation
432
+ this.device.publish(topic, message);
433
+ }
434
+
435
+ public handleMessageTopic({
436
+ topic,
437
+ message,
438
+ }: {
439
+ topic: string;
440
+ message: ClientMessage;
441
+ }) {
442
+ if (topic === this.subsribedTopics.response) {
443
+ this.handleCloudResponse(message);
444
+ } else {
445
+ this.handleClientMessage(message);
446
+ }
447
+ }
448
+ }
449
+
450
+ export function runDeviceAgentCloudInterface() {
451
+ const deviceAgent = new DeviceAgentCloudConnection();
452
+
307
453
  deviceAgent.device.on('connect', function () {
308
454
  deviceAgent.publishMessage('connection', deviceAgent.getClientId());
309
455
  console.log('Device Agent has connected to the cloud');
@@ -313,12 +459,17 @@ export function runDeviceAgentCloudInterface() {
313
459
  console.log('Device Agent has been disconnected from the cloud');
314
460
  });
315
461
 
316
- deviceAgent.device.on('message', function (topic, payload) {
317
- // ToDo: insert valdiation here for incoming messages. Maybe we will use a JSON schema for the message structure.
462
+ deviceAgent.device.on('message', function (topic: string, payload: string) {
318
463
  try {
319
- handleMessageTopic({ topic, payload: JSON.parse(payload) });
464
+ const jsonPacket = JSON.parse(payload);
465
+ const valid = validateClientMessage(jsonPacket);
466
+ if (!valid) {
467
+ logger.error(JSON.stringify(validateClientMessage.errors));
468
+ } else {
469
+ deviceAgent.handleMessageTopic({ topic, message: jsonPacket });
470
+ }
320
471
  } catch (error) {
321
- console.error(error);
472
+ logger.error(error);
322
473
  }
323
474
  });
324
475
  }