@dronedeploy/rocos-js-sdk 4.2.1 → 4.3.0-rc.1

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.
@@ -1,9 +1,10 @@
1
- import { AssetStorageService, AuthService, CallerService, CommandService, ConfigGroupService, ControlService, DashboardService, DeviceCredentialsService, EnvironmentService, EvaluatorService, EventService, FileAccessorService, IntegrationService, MapService, ProfileService, ProjectService, RTPWebRTCService, RobotService, SearchService, SpotProvisioningService, SpotProvisioningServiceNode, StreamService, TagService, TargetService, TelemetryService, TimeSyncerService, UserService, WebRTCSignallingService, WorkflowService } from './services';
1
+ import { AssetStorageService, AuthService, CallerService, CommandService, ConfigGroupService, ControlService, DashboardService, DeviceCredentialsService, EnvironmentService, EvaluatorService, EventService, FileAccessorService, IntegrationService, MapService, ProfileService, ProjectService, RTPWebRTCService, RobotService, RobotStatusService, SearchService, SpotProvisioningService, SpotProvisioningServiceNode, StreamService, TagService, TargetService, TelemetryService, TimeSyncerService, UserService, WebRTCSignallingService, WorkflowService } from './services';
2
2
  import { IBaseService, IDebugLevel, ServiceEnum } from './models';
3
3
  export declare abstract class IRocosSDK {
4
4
  abstract getService<T extends IBaseService>(_name: ServiceEnum): T;
5
5
  abstract getAuthService(): AuthService;
6
6
  abstract getRobotService(): RobotService;
7
+ abstract getRobotStatusService(): RobotStatusService;
7
8
  abstract getEventService(): EventService;
8
9
  abstract getEnvironmentService(): EnvironmentService;
9
10
  abstract getProjectService(): ProjectService;
package/cjs/RocosSDK.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AssetStorageService, AuthService, CallerService, CommandService, ConfigGroupService, ControlService, DashboardService, DeviceCredentialsService, EnvironmentService, EvaluatorService, EventService, FileAccessorService, IntegrationService, MapService, ProfileService, ProjectService, RTPWebRTCService, RobotService, ScheduleService, SearchService, SpotProvisioningService, StreamService, TagService, TargetService, TelemetryService, TimeSyncerService, UserService, WebRTCSignallingService, WorkflowService } from './services';
1
+ import { AssetStorageService, AuthService, CallerService, CommandService, ConfigGroupService, ControlService, DashboardService, DeviceCredentialsService, EnvironmentService, EvaluatorService, EventService, FileAccessorService, IntegrationService, MapService, ProfileService, ProjectService, RTPWebRTCService, RobotService, RobotStatusService, ScheduleService, SearchService, SpotProvisioningService, StreamService, TagService, TargetService, TelemetryService, TimeSyncerService, UserService, WebRTCSignallingService, WorkflowService } from './services';
2
2
  import { AuthorisedConfig, IBaseService, IDebugLevel, IRocosSDKConfig, ServiceEnum } from './models';
3
3
  import { IRocosSDK } from './IRocosSDK';
4
4
  import { Logger } from 'loglevel';
@@ -26,6 +26,10 @@ export declare class RocosSDK implements IRocosSDK {
26
26
  * Gets the robot service.
27
27
  */
28
28
  getRobotService(): RobotService;
29
+ /**
30
+ * Gets the robot status service.
31
+ */
32
+ getRobotStatusService(): RobotStatusService;
29
33
  /**
30
34
  * Gets the event service.
31
35
  */
package/cjs/RocosSDK.js CHANGED
@@ -38,6 +38,9 @@ class RocosSDK {
38
38
  case models_1.ServiceEnum.ROBOT:
39
39
  this.services[name] = new services_1.RobotService(config);
40
40
  break;
41
+ case models_1.ServiceEnum.ROBOT_STATUS:
42
+ this.services[name] = new services_1.RobotStatusService(config);
43
+ break;
41
44
  case models_1.ServiceEnum.ENVIRONMENT:
42
45
  this.services[name] = new services_1.EnvironmentService(config);
43
46
  break;
@@ -133,6 +136,12 @@ class RocosSDK {
133
136
  getRobotService() {
134
137
  return this.getService(models_1.ServiceEnum.ROBOT);
135
138
  }
139
+ /**
140
+ * Gets the robot status service.
141
+ */
142
+ getRobotStatusService() {
143
+ return this.getService(models_1.ServiceEnum.ROBOT_STATUS);
144
+ }
136
145
  /**
137
146
  * Gets the event service.
138
147
  */
@@ -28,6 +28,7 @@ export declare const errorCodes: {
28
28
  TARGET_SERVICE_ERROR: string;
29
29
  ENVIRONMENT_SERVICE_ERROR: string;
30
30
  TAG_SERVICE_ERROR: string;
31
+ ROBOT_STATUS_SERVICE_ERROR: string;
31
32
  };
32
33
  export declare class RocosError extends Error {
33
34
  code?: string;
@@ -31,6 +31,7 @@ exports.errorCodes = {
31
31
  TARGET_SERVICE_ERROR: 'TARGET_SERVICE_ERROR',
32
32
  ENVIRONMENT_SERVICE_ERROR: 'ENVIRONMENT_SERVICE_ERROR',
33
33
  TAG_SERVICE_ERROR: 'TAG_SERVICE_ERROR',
34
+ ROBOT_STATUS_SERVICE_ERROR: 'ROBOT_STATUS_SERVICE_ERROR',
34
35
  };
35
36
  class RocosError extends Error {
36
37
  constructor(err, code, statusCode) {
@@ -17,6 +17,7 @@ export declare enum ServiceEnum {
17
17
  PROJECT = "project",
18
18
  PROFILE = "profile",
19
19
  ROBOT = "robot",
20
+ ROBOT_STATUS = "robot_status",
20
21
  RTP_WEBRTC = "rtp_webrtc",
21
22
  SCHEDULE = "schedule",
22
23
  SEARCH = "search",
@@ -21,6 +21,7 @@ var ServiceEnum;
21
21
  ServiceEnum["PROJECT"] = "project";
22
22
  ServiceEnum["PROFILE"] = "profile";
23
23
  ServiceEnum["ROBOT"] = "robot";
24
+ ServiceEnum["ROBOT_STATUS"] = "robot_status";
24
25
  ServiceEnum["RTP_WEBRTC"] = "rtp_webrtc";
25
26
  ServiceEnum["SCHEDULE"] = "schedule";
26
27
  ServiceEnum["SEARCH"] = "search";
@@ -0,0 +1,37 @@
1
+ import { IBaseService, IRocosSDKConfig, RobotConnectivityStatus, RobotStatus, RocosError } from '../models';
2
+ import { Observable } from 'rxjs';
3
+ import { ReportedRobotStatus } from './robot-status/reportedStatus';
4
+ import { BaseServiceAbstract } from './BaseServiceAbstract';
5
+ /** Per-call tuning for the robot status observables. */
6
+ export interface RobotStatusOptions {
7
+ /** Services whose connectivity gates ONLINE/DEGRADED. Defaults to the standard expected-services set. */
8
+ expectedServices?: string[];
9
+ /** How long without a heartbeat before telemetry-liveness is considered dead (ms). */
10
+ heartbeatTimeoutMs?: number;
11
+ /** How often the connections endpoints are polled (ms, floored internally). */
12
+ connectivityPollMs?: number;
13
+ }
14
+ /**
15
+ * The single source of truth for a robot's status. It composes the reported
16
+ * status (health/readiness from the heartbeat), per-service connectivity, and
17
+ * the canonical indicator colour, so consumers subscribe to one observable
18
+ * rather than deriving "is the robot online" themselves.
19
+ */
20
+ export declare class RobotStatusService extends BaseServiceAbstract implements IBaseService {
21
+ constructor(config: IRocosSDKConfig);
22
+ getStatus(): boolean;
23
+ /** Health + readiness as reported by the robot (new topic, legacy heartbeat fallback). */
24
+ getReportedStatusChanges(projectId: string, callsign: string): Observable<ReportedRobotStatus>;
25
+ /** Per-service connectivity plus the aggregate verdict, polled. */
26
+ getConnectivityChanges(projectId: string, callsign: string, opts?: RobotStatusOptions): Observable<RobotConnectivityStatus>;
27
+ /** One-shot connectivity snapshot: resolves once every expected service is known, or after a timeout. */
28
+ getConnectivity(projectId: string, callsign: string, opts?: RobotStatusOptions): Promise<RobotConnectivityStatus>;
29
+ /**
30
+ * The headline stream: the combined {@link RobotStatus} including the canonical
31
+ * colour token. This is what status indicators subscribe to.
32
+ */
33
+ getRobotStatusChanges(projectId: string, callsign: string, opts?: RobotStatusOptions): Observable<RobotStatus>;
34
+ protected getError(e: Error): RocosError;
35
+ private get telemetryService();
36
+ private connectivityDeps;
37
+ }
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RobotStatusService = void 0;
4
+ const connectivity_1 = require("./robot-status/connectivity");
5
+ const models_1 = require("../models");
6
+ const rxjs_1 = require("rxjs");
7
+ const reportedStatus_1 = require("./robot-status/reportedStatus");
8
+ const BaseServiceAbstract_1 = require("./BaseServiceAbstract");
9
+ const RocosLogger_1 = require("../logger/RocosLogger");
10
+ const RocosStore_1 = require("../store/RocosStore");
11
+ const getRobotStatusColour_1 = require("../helpers/getRobotStatusColour");
12
+ /** Upper bound on how long the one-shot {@link RobotStatusService.getConnectivity} waits to settle. */
13
+ const ONE_SHOT_TIMEOUT_MS = 10000;
14
+ const isKnown = (status) => {
15
+ return Object.values(status.services).every((connection) => connection !== models_1.ServiceConnection.UNKNOWN);
16
+ };
17
+ const robotStatusEquals = (a, b) => {
18
+ if (a.health !== b.health || a.readiness !== b.readiness)
19
+ return false;
20
+ if (a.connectivity.overall !== b.connectivity.overall)
21
+ return false;
22
+ const keys = Object.keys(a.connectivity.services);
23
+ if (keys.length !== Object.keys(b.connectivity.services).length)
24
+ return false;
25
+ return keys.every((key) => a.connectivity.services[key] === b.connectivity.services[key]);
26
+ };
27
+ /**
28
+ * The single source of truth for a robot's status. It composes the reported
29
+ * status (health/readiness from the heartbeat), per-service connectivity, and
30
+ * the canonical indicator colour, so consumers subscribe to one observable
31
+ * rather than deriving "is the robot online" themselves.
32
+ */
33
+ class RobotStatusService extends BaseServiceAbstract_1.BaseServiceAbstract {
34
+ constructor(config) {
35
+ super(config);
36
+ this.logger = RocosLogger_1.RocosLogger.getInstance(`RobotStatusService(${this.config.url})`);
37
+ }
38
+ getStatus() {
39
+ return true;
40
+ }
41
+ /** Health + readiness as reported by the robot (new topic, legacy heartbeat fallback). */
42
+ getReportedStatusChanges(projectId, callsign) {
43
+ return (0, reportedStatus_1.getReportedStatusChanges)(this.telemetryService, projectId, callsign);
44
+ }
45
+ /** Per-service connectivity plus the aggregate verdict, polled. */
46
+ getConnectivityChanges(projectId, callsign, opts) {
47
+ return (0, connectivity_1.getConnectivityChanges)(this.connectivityDeps(projectId, callsign, opts), projectId, callsign, opts?.expectedServices, opts?.connectivityPollMs);
48
+ }
49
+ /** One-shot connectivity snapshot: resolves once every expected service is known, or after a timeout. */
50
+ getConnectivity(projectId, callsign, opts) {
51
+ // Capture the latest snapshot explicitly: on timeout, refCount teardown would otherwise
52
+ // hand a fresh (all-UNKNOWN seed) re-subscription to the fallback rather than the last value.
53
+ let latest;
54
+ const changes$ = this.getConnectivityChanges(projectId, callsign, opts).pipe((0, rxjs_1.tap)((status) => {
55
+ latest = status;
56
+ }), (0, rxjs_1.shareReplay)({ bufferSize: 1, refCount: true }));
57
+ return (0, rxjs_1.firstValueFrom)(changes$.pipe((0, rxjs_1.first)(isKnown),
58
+ // Don't wait forever on a persistently-unknown service — fall back to the latest snapshot seen.
59
+ (0, rxjs_1.timeout)({ first: ONE_SHOT_TIMEOUT_MS, with: () => (latest ? (0, rxjs_1.of)(latest) : changes$.pipe((0, rxjs_1.take)(1))) })));
60
+ }
61
+ /**
62
+ * The headline stream: the combined {@link RobotStatus} including the canonical
63
+ * colour token. This is what status indicators subscribe to.
64
+ */
65
+ getRobotStatusChanges(projectId, callsign, opts) {
66
+ const reported$ = this.getReportedStatusChanges(projectId, callsign).pipe(
67
+ // Seed so the combined stream emits (e.g. gray) before the first heartbeat arrives.
68
+ (0, rxjs_1.startWith)({ health: models_1.RobotHealth.UNKNOWN, readiness: models_1.RobotReadiness.UNKNOWN }));
69
+ const connectivity$ = this.getConnectivityChanges(projectId, callsign, opts);
70
+ return (0, rxjs_1.combineLatest)([reported$, connectivity$]).pipe((0, rxjs_1.map)(([reported, connectivity]) => ({
71
+ health: reported.health,
72
+ readiness: reported.readiness,
73
+ connectivity,
74
+ colour: (0, getRobotStatusColour_1.getRobotStatusColour)({ health: reported.health, readiness: reported.readiness, connectivity }),
75
+ })), (0, rxjs_1.distinctUntilChanged)(robotStatusEquals));
76
+ }
77
+ getError(e) {
78
+ return new models_1.RocosError(e, models_1.errorCodes.ROBOT_STATUS_SERVICE_ERROR);
79
+ }
80
+ get telemetryService() {
81
+ return RocosStore_1.RocosStore.getSDKInstance(this.config).getTelemetryService();
82
+ }
83
+ connectivityDeps(projectId, callsign, opts) {
84
+ return {
85
+ httpGet: (url) => {
86
+ return this.callGet(url, `Failed to get connection status for ${callsign} in project ${projectId}.`);
87
+ },
88
+ baseUrl: this.config.url,
89
+ insecure: this.config.insecure,
90
+ telemetryLiveness$: (0, reportedStatus_1.getTelemetryLiveness)(this.telemetryService, projectId, callsign, opts?.heartbeatTimeoutMs),
91
+ };
92
+ }
93
+ }
94
+ exports.RobotStatusService = RobotStatusService;
@@ -17,6 +17,7 @@ export * from './ProfileService';
17
17
  export * from './ProjectService';
18
18
  export * from './RTPWebRTCService';
19
19
  export * from './RobotService';
20
+ export * from './RobotStatusService';
20
21
  export * from './ScheduleService';
21
22
  export * from './SearchService';
22
23
  export * from './SpotProvisioningService';
@@ -33,6 +33,7 @@ __exportStar(require("./ProfileService"), exports);
33
33
  __exportStar(require("./ProjectService"), exports);
34
34
  __exportStar(require("./RTPWebRTCService"), exports);
35
35
  __exportStar(require("./RobotService"), exports);
36
+ __exportStar(require("./RobotStatusService"), exports);
36
37
  __exportStar(require("./ScheduleService"), exports);
37
38
  __exportStar(require("./SearchService"), exports);
38
39
  __exportStar(require("./SpotProvisioningService"), exports);
@@ -17,6 +17,14 @@ export declare const TELEMETRY_LIVENESS_SERVICE = "telemetry-liveness";
17
17
  * this list once Teletubby exposes it (follow-up).
18
18
  */
19
19
  export declare const DEFAULT_EXPECTED_SERVICES: string[];
20
+ /**
21
+ * Floor for the connections-endpoint poll interval. Guards against a caller
22
+ * passing `0` (or any tiny value), which would otherwise poll the API on every
23
+ * event-loop tick.
24
+ */
25
+ export declare const MIN_CONNECTIVITY_POLL_MS = 250;
26
+ /** Clamps a requested poll interval up to {@link MIN_CONNECTIVITY_POLL_MS}. */
27
+ export declare const clampPollInterval: (pollMs: number) => number;
20
28
  /** Authenticated GET used to poll a service's `connections` endpoint. */
21
29
  export type ConnectionsHttpGet = (url: string) => Promise<IConnectedCallsign>;
22
30
  /**
@@ -47,6 +55,9 @@ export declare const aggregateConnectivity: (services: Record<string, ServiceCon
47
55
  * {@link ServiceConnection} (change-only). A failed/timed-out poll yields
48
56
  * `UNKNOWN` (held transitional) rather than `DISCONNECTED`, to avoid a spurious
49
57
  * degraded flash on a transient error.
58
+ *
59
+ * `pollMs` is floored at {@link MIN_CONNECTIVITY_POLL_MS} so the endpoint can't
60
+ * be polled unreasonably fast.
50
61
  */
51
62
  export declare const getServiceConnectionChanges: (httpGet: ConnectionsHttpGet, url: string, pollMs?: number) => Observable<ServiceConnection>;
52
63
  export interface ConnectivityDeps {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getConnectivityChanges = exports.getServiceConnectionChanges = exports.aggregateConnectivity = exports.mapConnectionStatus = exports.buildConnectionsUrl = exports.DEFAULT_EXPECTED_SERVICES = exports.TELEMETRY_LIVENESS_SERVICE = exports.CONNECTION_SERVICE_PREFIXES = void 0;
3
+ exports.getConnectivityChanges = exports.getServiceConnectionChanges = exports.aggregateConnectivity = exports.mapConnectionStatus = exports.buildConnectionsUrl = exports.clampPollInterval = exports.MIN_CONNECTIVITY_POLL_MS = exports.DEFAULT_EXPECTED_SERVICES = exports.TELEMETRY_LIVENESS_SERVICE = exports.CONNECTION_SERVICE_PREFIXES = void 0;
4
4
  const api_1 = require("../../constants/api");
5
5
  const models_1 = require("../../models");
6
6
  const rxjs_1 = require("rxjs");
@@ -26,6 +26,15 @@ exports.TELEMETRY_LIVENESS_SERVICE = 'telemetry-liveness';
26
26
  */
27
27
  exports.DEFAULT_EXPECTED_SERVICES = [exports.TELEMETRY_LIVENESS_SERVICE, 'robot-configs', 'robot-services'];
28
28
  const DEFAULT_CONNECTIVITY_POLL_MS = 5000;
29
+ /**
30
+ * Floor for the connections-endpoint poll interval. Guards against a caller
31
+ * passing `0` (or any tiny value), which would otherwise poll the API on every
32
+ * event-loop tick.
33
+ */
34
+ exports.MIN_CONNECTIVITY_POLL_MS = 250;
35
+ /** Clamps a requested poll interval up to {@link MIN_CONNECTIVITY_POLL_MS}. */
36
+ const clampPollInterval = (pollMs) => Math.max(pollMs, exports.MIN_CONNECTIVITY_POLL_MS);
37
+ exports.clampPollInterval = clampPollInterval;
29
38
  /**
30
39
  * Builds the `connections` endpoint URL for a service — per-callsign when a
31
40
  * callsign is given, otherwise the bulk per-project variant.
@@ -78,9 +87,12 @@ exports.aggregateConnectivity = aggregateConnectivity;
78
87
  * {@link ServiceConnection} (change-only). A failed/timed-out poll yields
79
88
  * `UNKNOWN` (held transitional) rather than `DISCONNECTED`, to avoid a spurious
80
89
  * degraded flash on a transient error.
90
+ *
91
+ * `pollMs` is floored at {@link MIN_CONNECTIVITY_POLL_MS} so the endpoint can't
92
+ * be polled unreasonably fast.
81
93
  */
82
94
  const getServiceConnectionChanges = (httpGet, url, pollMs = DEFAULT_CONNECTIVITY_POLL_MS) => {
83
- return (0, rxjs_1.timer)(0, pollMs).pipe((0, rxjs_1.switchMap)(() => {
95
+ return (0, rxjs_1.timer)(0, (0, exports.clampPollInterval)(pollMs)).pipe((0, rxjs_1.switchMap)(() => {
84
96
  return (0, rxjs_1.from)(httpGet(url)).pipe((0, rxjs_1.map)(exports.mapConnectionStatus), (0, rxjs_1.catchError)(() => (0, rxjs_1.of)(models_1.ServiceConnection.UNKNOWN)));
85
97
  }), (0, rxjs_1.startWith)(models_1.ServiceConnection.UNKNOWN), (0, rxjs_1.distinctUntilChanged)());
86
98
  };
@@ -1,9 +1,10 @@
1
- import { AssetStorageService, AuthService, CallerService, CommandService, ConfigGroupService, ControlService, DashboardService, DeviceCredentialsService, EnvironmentService, EvaluatorService, EventService, FileAccessorService, IntegrationService, MapService, ProfileService, ProjectService, RTPWebRTCService, RobotService, SearchService, SpotProvisioningService, SpotProvisioningServiceNode, StreamService, TagService, TargetService, TelemetryService, TimeSyncerService, UserService, WebRTCSignallingService, WorkflowService } from './services';
1
+ import { AssetStorageService, AuthService, CallerService, CommandService, ConfigGroupService, ControlService, DashboardService, DeviceCredentialsService, EnvironmentService, EvaluatorService, EventService, FileAccessorService, IntegrationService, MapService, ProfileService, ProjectService, RTPWebRTCService, RobotService, RobotStatusService, SearchService, SpotProvisioningService, SpotProvisioningServiceNode, StreamService, TagService, TargetService, TelemetryService, TimeSyncerService, UserService, WebRTCSignallingService, WorkflowService } from './services';
2
2
  import { IBaseService, IDebugLevel, ServiceEnum } from './models';
3
3
  export declare abstract class IRocosSDK {
4
4
  abstract getService<T extends IBaseService>(_name: ServiceEnum): T;
5
5
  abstract getAuthService(): AuthService;
6
6
  abstract getRobotService(): RobotService;
7
+ abstract getRobotStatusService(): RobotStatusService;
7
8
  abstract getEventService(): EventService;
8
9
  abstract getEnvironmentService(): EnvironmentService;
9
10
  abstract getProjectService(): ProjectService;
package/esm/RocosSDK.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AssetStorageService, AuthService, CallerService, CommandService, ConfigGroupService, ControlService, DashboardService, DeviceCredentialsService, EnvironmentService, EvaluatorService, EventService, FileAccessorService, IntegrationService, MapService, ProfileService, ProjectService, RTPWebRTCService, RobotService, ScheduleService, SearchService, SpotProvisioningService, StreamService, TagService, TargetService, TelemetryService, TimeSyncerService, UserService, WebRTCSignallingService, WorkflowService } from './services';
1
+ import { AssetStorageService, AuthService, CallerService, CommandService, ConfigGroupService, ControlService, DashboardService, DeviceCredentialsService, EnvironmentService, EvaluatorService, EventService, FileAccessorService, IntegrationService, MapService, ProfileService, ProjectService, RTPWebRTCService, RobotService, RobotStatusService, ScheduleService, SearchService, SpotProvisioningService, StreamService, TagService, TargetService, TelemetryService, TimeSyncerService, UserService, WebRTCSignallingService, WorkflowService } from './services';
2
2
  import { AuthorisedConfig, IBaseService, IDebugLevel, IRocosSDKConfig, ServiceEnum } from './models';
3
3
  import { IRocosSDK } from './IRocosSDK';
4
4
  import { Logger } from 'loglevel';
@@ -26,6 +26,10 @@ export declare class RocosSDK implements IRocosSDK {
26
26
  * Gets the robot service.
27
27
  */
28
28
  getRobotService(): RobotService;
29
+ /**
30
+ * Gets the robot status service.
31
+ */
32
+ getRobotStatusService(): RobotStatusService;
29
33
  /**
30
34
  * Gets the event service.
31
35
  */
package/esm/RocosSDK.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AssetStorageService, AuthService, CallerService, CommandService, ConfigGroupService, ControlService, DashboardService, DeviceCredentialsService, EnvironmentService, EvaluatorService, EventService, FileAccessorService, IntegrationService, MapService, PlatFormTimeService, ProfileService, ProjectService, RTPWebRTCService, RobotService, ScheduleService, SearchService, SpotProvisioningService, StreamService, TagService, TargetService, TelemetryService, TimeSyncerService, UserService, WebRTCSignallingService, WorkflowService, } from './services';
1
+ import { AssetStorageService, AuthService, CallerService, CommandService, ConfigGroupService, ControlService, DashboardService, DeviceCredentialsService, EnvironmentService, EvaluatorService, EventService, FileAccessorService, IntegrationService, MapService, PlatFormTimeService, ProfileService, ProjectService, RTPWebRTCService, RobotService, RobotStatusService, ScheduleService, SearchService, SpotProvisioningService, StreamService, TagService, TargetService, TelemetryService, TimeSyncerService, UserService, WebRTCSignallingService, WorkflowService, } from './services';
2
2
  import { ServiceEnum } from './models';
3
3
  import { RocosLogger } from './logger/RocosLogger';
4
4
  import { RocosStore } from './store/RocosStore';
@@ -35,6 +35,9 @@ export class RocosSDK {
35
35
  case ServiceEnum.ROBOT:
36
36
  this.services[name] = new RobotService(config);
37
37
  break;
38
+ case ServiceEnum.ROBOT_STATUS:
39
+ this.services[name] = new RobotStatusService(config);
40
+ break;
38
41
  case ServiceEnum.ENVIRONMENT:
39
42
  this.services[name] = new EnvironmentService(config);
40
43
  break;
@@ -130,6 +133,12 @@ export class RocosSDK {
130
133
  getRobotService() {
131
134
  return this.getService(ServiceEnum.ROBOT);
132
135
  }
136
+ /**
137
+ * Gets the robot status service.
138
+ */
139
+ getRobotStatusService() {
140
+ return this.getService(ServiceEnum.ROBOT_STATUS);
141
+ }
133
142
  /**
134
143
  * Gets the event service.
135
144
  */
@@ -28,6 +28,7 @@ export declare const errorCodes: {
28
28
  TARGET_SERVICE_ERROR: string;
29
29
  ENVIRONMENT_SERVICE_ERROR: string;
30
30
  TAG_SERVICE_ERROR: string;
31
+ ROBOT_STATUS_SERVICE_ERROR: string;
31
32
  };
32
33
  export declare class RocosError extends Error {
33
34
  code?: string;
@@ -28,6 +28,7 @@ export const errorCodes = {
28
28
  TARGET_SERVICE_ERROR: 'TARGET_SERVICE_ERROR',
29
29
  ENVIRONMENT_SERVICE_ERROR: 'ENVIRONMENT_SERVICE_ERROR',
30
30
  TAG_SERVICE_ERROR: 'TAG_SERVICE_ERROR',
31
+ ROBOT_STATUS_SERVICE_ERROR: 'ROBOT_STATUS_SERVICE_ERROR',
31
32
  };
32
33
  export class RocosError extends Error {
33
34
  constructor(err, code, statusCode) {
@@ -17,6 +17,7 @@ export declare enum ServiceEnum {
17
17
  PROJECT = "project",
18
18
  PROFILE = "profile",
19
19
  ROBOT = "robot",
20
+ ROBOT_STATUS = "robot_status",
20
21
  RTP_WEBRTC = "rtp_webrtc",
21
22
  SCHEDULE = "schedule",
22
23
  SEARCH = "search",
@@ -18,6 +18,7 @@ export var ServiceEnum;
18
18
  ServiceEnum["PROJECT"] = "project";
19
19
  ServiceEnum["PROFILE"] = "profile";
20
20
  ServiceEnum["ROBOT"] = "robot";
21
+ ServiceEnum["ROBOT_STATUS"] = "robot_status";
21
22
  ServiceEnum["RTP_WEBRTC"] = "rtp_webrtc";
22
23
  ServiceEnum["SCHEDULE"] = "schedule";
23
24
  ServiceEnum["SEARCH"] = "search";
@@ -0,0 +1,37 @@
1
+ import { IBaseService, IRocosSDKConfig, RobotConnectivityStatus, RobotStatus, RocosError } from '../models';
2
+ import { Observable } from 'rxjs';
3
+ import { ReportedRobotStatus } from './robot-status/reportedStatus';
4
+ import { BaseServiceAbstract } from './BaseServiceAbstract';
5
+ /** Per-call tuning for the robot status observables. */
6
+ export interface RobotStatusOptions {
7
+ /** Services whose connectivity gates ONLINE/DEGRADED. Defaults to the standard expected-services set. */
8
+ expectedServices?: string[];
9
+ /** How long without a heartbeat before telemetry-liveness is considered dead (ms). */
10
+ heartbeatTimeoutMs?: number;
11
+ /** How often the connections endpoints are polled (ms, floored internally). */
12
+ connectivityPollMs?: number;
13
+ }
14
+ /**
15
+ * The single source of truth for a robot's status. It composes the reported
16
+ * status (health/readiness from the heartbeat), per-service connectivity, and
17
+ * the canonical indicator colour, so consumers subscribe to one observable
18
+ * rather than deriving "is the robot online" themselves.
19
+ */
20
+ export declare class RobotStatusService extends BaseServiceAbstract implements IBaseService {
21
+ constructor(config: IRocosSDKConfig);
22
+ getStatus(): boolean;
23
+ /** Health + readiness as reported by the robot (new topic, legacy heartbeat fallback). */
24
+ getReportedStatusChanges(projectId: string, callsign: string): Observable<ReportedRobotStatus>;
25
+ /** Per-service connectivity plus the aggregate verdict, polled. */
26
+ getConnectivityChanges(projectId: string, callsign: string, opts?: RobotStatusOptions): Observable<RobotConnectivityStatus>;
27
+ /** One-shot connectivity snapshot: resolves once every expected service is known, or after a timeout. */
28
+ getConnectivity(projectId: string, callsign: string, opts?: RobotStatusOptions): Promise<RobotConnectivityStatus>;
29
+ /**
30
+ * The headline stream: the combined {@link RobotStatus} including the canonical
31
+ * colour token. This is what status indicators subscribe to.
32
+ */
33
+ getRobotStatusChanges(projectId: string, callsign: string, opts?: RobotStatusOptions): Observable<RobotStatus>;
34
+ protected getError(e: Error): RocosError;
35
+ private get telemetryService();
36
+ private connectivityDeps;
37
+ }
@@ -0,0 +1,90 @@
1
+ import { getConnectivityChanges } from './robot-status/connectivity';
2
+ import { RobotHealth, RobotReadiness, RocosError, ServiceConnection, errorCodes, } from '../models';
3
+ import { combineLatest, distinctUntilChanged, first, firstValueFrom, map, of, shareReplay, startWith, take, tap, timeout, } from 'rxjs';
4
+ import { getReportedStatusChanges, getTelemetryLiveness } from './robot-status/reportedStatus';
5
+ import { BaseServiceAbstract } from './BaseServiceAbstract';
6
+ import { RocosLogger } from '../logger/RocosLogger';
7
+ import { RocosStore } from '../store/RocosStore';
8
+ import { getRobotStatusColour } from '../helpers/getRobotStatusColour';
9
+ /** Upper bound on how long the one-shot {@link RobotStatusService.getConnectivity} waits to settle. */
10
+ const ONE_SHOT_TIMEOUT_MS = 10000;
11
+ const isKnown = (status) => {
12
+ return Object.values(status.services).every((connection) => connection !== ServiceConnection.UNKNOWN);
13
+ };
14
+ const robotStatusEquals = (a, b) => {
15
+ if (a.health !== b.health || a.readiness !== b.readiness)
16
+ return false;
17
+ if (a.connectivity.overall !== b.connectivity.overall)
18
+ return false;
19
+ const keys = Object.keys(a.connectivity.services);
20
+ if (keys.length !== Object.keys(b.connectivity.services).length)
21
+ return false;
22
+ return keys.every((key) => a.connectivity.services[key] === b.connectivity.services[key]);
23
+ };
24
+ /**
25
+ * The single source of truth for a robot's status. It composes the reported
26
+ * status (health/readiness from the heartbeat), per-service connectivity, and
27
+ * the canonical indicator colour, so consumers subscribe to one observable
28
+ * rather than deriving "is the robot online" themselves.
29
+ */
30
+ export class RobotStatusService extends BaseServiceAbstract {
31
+ constructor(config) {
32
+ super(config);
33
+ this.logger = RocosLogger.getInstance(`RobotStatusService(${this.config.url})`);
34
+ }
35
+ getStatus() {
36
+ return true;
37
+ }
38
+ /** Health + readiness as reported by the robot (new topic, legacy heartbeat fallback). */
39
+ getReportedStatusChanges(projectId, callsign) {
40
+ return getReportedStatusChanges(this.telemetryService, projectId, callsign);
41
+ }
42
+ /** Per-service connectivity plus the aggregate verdict, polled. */
43
+ getConnectivityChanges(projectId, callsign, opts) {
44
+ return getConnectivityChanges(this.connectivityDeps(projectId, callsign, opts), projectId, callsign, opts?.expectedServices, opts?.connectivityPollMs);
45
+ }
46
+ /** One-shot connectivity snapshot: resolves once every expected service is known, or after a timeout. */
47
+ getConnectivity(projectId, callsign, opts) {
48
+ // Capture the latest snapshot explicitly: on timeout, refCount teardown would otherwise
49
+ // hand a fresh (all-UNKNOWN seed) re-subscription to the fallback rather than the last value.
50
+ let latest;
51
+ const changes$ = this.getConnectivityChanges(projectId, callsign, opts).pipe(tap((status) => {
52
+ latest = status;
53
+ }), shareReplay({ bufferSize: 1, refCount: true }));
54
+ return firstValueFrom(changes$.pipe(first(isKnown),
55
+ // Don't wait forever on a persistently-unknown service — fall back to the latest snapshot seen.
56
+ timeout({ first: ONE_SHOT_TIMEOUT_MS, with: () => (latest ? of(latest) : changes$.pipe(take(1))) })));
57
+ }
58
+ /**
59
+ * The headline stream: the combined {@link RobotStatus} including the canonical
60
+ * colour token. This is what status indicators subscribe to.
61
+ */
62
+ getRobotStatusChanges(projectId, callsign, opts) {
63
+ const reported$ = this.getReportedStatusChanges(projectId, callsign).pipe(
64
+ // Seed so the combined stream emits (e.g. gray) before the first heartbeat arrives.
65
+ startWith({ health: RobotHealth.UNKNOWN, readiness: RobotReadiness.UNKNOWN }));
66
+ const connectivity$ = this.getConnectivityChanges(projectId, callsign, opts);
67
+ return combineLatest([reported$, connectivity$]).pipe(map(([reported, connectivity]) => ({
68
+ health: reported.health,
69
+ readiness: reported.readiness,
70
+ connectivity,
71
+ colour: getRobotStatusColour({ health: reported.health, readiness: reported.readiness, connectivity }),
72
+ })), distinctUntilChanged(robotStatusEquals));
73
+ }
74
+ getError(e) {
75
+ return new RocosError(e, errorCodes.ROBOT_STATUS_SERVICE_ERROR);
76
+ }
77
+ get telemetryService() {
78
+ return RocosStore.getSDKInstance(this.config).getTelemetryService();
79
+ }
80
+ connectivityDeps(projectId, callsign, opts) {
81
+ return {
82
+ httpGet: (url) => {
83
+ return this.callGet(url, `Failed to get connection status for ${callsign} in project ${projectId}.`);
84
+ },
85
+ baseUrl: this.config.url,
86
+ insecure: this.config.insecure,
87
+ telemetryLiveness$: getTelemetryLiveness(this.telemetryService, projectId, callsign, opts?.heartbeatTimeoutMs),
88
+ };
89
+ }
90
+ }
@@ -17,6 +17,7 @@ export * from './ProfileService';
17
17
  export * from './ProjectService';
18
18
  export * from './RTPWebRTCService';
19
19
  export * from './RobotService';
20
+ export * from './RobotStatusService';
20
21
  export * from './ScheduleService';
21
22
  export * from './SearchService';
22
23
  export * from './SpotProvisioningService';
@@ -17,6 +17,7 @@ export * from './ProfileService';
17
17
  export * from './ProjectService';
18
18
  export * from './RTPWebRTCService';
19
19
  export * from './RobotService';
20
+ export * from './RobotStatusService';
20
21
  export * from './ScheduleService';
21
22
  export * from './SearchService';
22
23
  export * from './SpotProvisioningService';
@@ -17,6 +17,14 @@ export declare const TELEMETRY_LIVENESS_SERVICE = "telemetry-liveness";
17
17
  * this list once Teletubby exposes it (follow-up).
18
18
  */
19
19
  export declare const DEFAULT_EXPECTED_SERVICES: string[];
20
+ /**
21
+ * Floor for the connections-endpoint poll interval. Guards against a caller
22
+ * passing `0` (or any tiny value), which would otherwise poll the API on every
23
+ * event-loop tick.
24
+ */
25
+ export declare const MIN_CONNECTIVITY_POLL_MS = 250;
26
+ /** Clamps a requested poll interval up to {@link MIN_CONNECTIVITY_POLL_MS}. */
27
+ export declare const clampPollInterval: (pollMs: number) => number;
20
28
  /** Authenticated GET used to poll a service's `connections` endpoint. */
21
29
  export type ConnectionsHttpGet = (url: string) => Promise<IConnectedCallsign>;
22
30
  /**
@@ -47,6 +55,9 @@ export declare const aggregateConnectivity: (services: Record<string, ServiceCon
47
55
  * {@link ServiceConnection} (change-only). A failed/timed-out poll yields
48
56
  * `UNKNOWN` (held transitional) rather than `DISCONNECTED`, to avoid a spurious
49
57
  * degraded flash on a transient error.
58
+ *
59
+ * `pollMs` is floored at {@link MIN_CONNECTIVITY_POLL_MS} so the endpoint can't
60
+ * be polled unreasonably fast.
50
61
  */
51
62
  export declare const getServiceConnectionChanges: (httpGet: ConnectionsHttpGet, url: string, pollMs?: number) => Observable<ServiceConnection>;
52
63
  export interface ConnectivityDeps {
@@ -23,6 +23,14 @@ export const TELEMETRY_LIVENESS_SERVICE = 'telemetry-liveness';
23
23
  */
24
24
  export const DEFAULT_EXPECTED_SERVICES = [TELEMETRY_LIVENESS_SERVICE, 'robot-configs', 'robot-services'];
25
25
  const DEFAULT_CONNECTIVITY_POLL_MS = 5000;
26
+ /**
27
+ * Floor for the connections-endpoint poll interval. Guards against a caller
28
+ * passing `0` (or any tiny value), which would otherwise poll the API on every
29
+ * event-loop tick.
30
+ */
31
+ export const MIN_CONNECTIVITY_POLL_MS = 250;
32
+ /** Clamps a requested poll interval up to {@link MIN_CONNECTIVITY_POLL_MS}. */
33
+ export const clampPollInterval = (pollMs) => Math.max(pollMs, MIN_CONNECTIVITY_POLL_MS);
26
34
  /**
27
35
  * Builds the `connections` endpoint URL for a service — per-callsign when a
28
36
  * callsign is given, otherwise the bulk per-project variant.
@@ -72,9 +80,12 @@ export const aggregateConnectivity = (services) => {
72
80
  * {@link ServiceConnection} (change-only). A failed/timed-out poll yields
73
81
  * `UNKNOWN` (held transitional) rather than `DISCONNECTED`, to avoid a spurious
74
82
  * degraded flash on a transient error.
83
+ *
84
+ * `pollMs` is floored at {@link MIN_CONNECTIVITY_POLL_MS} so the endpoint can't
85
+ * be polled unreasonably fast.
75
86
  */
76
87
  export const getServiceConnectionChanges = (httpGet, url, pollMs = DEFAULT_CONNECTIVITY_POLL_MS) => {
77
- return timer(0, pollMs).pipe(switchMap(() => {
88
+ return timer(0, clampPollInterval(pollMs)).pipe(switchMap(() => {
78
89
  return from(httpGet(url)).pipe(map(mapConnectionStatus), catchError(() => of(ServiceConnection.UNKNOWN)));
79
90
  }), startWith(ServiceConnection.UNKNOWN), distinctUntilChanged());
80
91
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dronedeploy/rocos-js-sdk",
3
- "version": "4.2.1",
3
+ "version": "4.3.0-rc.1",
4
4
  "description": "Javascript SDK for rocos",
5
5
  "main": "cjs/index.js",
6
6
  "module": "esm/index.js",