@dronedeploy/rocos-js-sdk 4.4.5 → 4.4.6

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.
@@ -62,6 +62,10 @@ class TelemetryStreamAbstract {
62
62
  }
63
63
  this.subscriberStatus = models_1.SubscriberStatusEnum.STOPPED;
64
64
  this.statusStream$.next(this.subscriberStatus);
65
+ // The stream is being torn down (the registry only stops a stream when it is evicted), so
66
+ // complete the status stream: this releases every service-level forwarder subscribed to it
67
+ // rather than leaving them attached to a dead instance.
68
+ this.statusStream$.complete();
65
69
  }
66
70
  addSubscription(params) {
67
71
  this.logger.info('Adding subscriptions from stream', params.uniqueId);
@@ -41,3 +41,26 @@ export interface RobotStatus {
41
41
  connectivity: RobotConnectivityStatus;
42
42
  colour: RobotStatusColour;
43
43
  }
44
+ /**
45
+ * One contributor to a reported-status axis, from the agent's
46
+ * `robot/status/contributors` topic. `status` is the contributor's own verdict
47
+ * for that axis — the integer wire value mapped onto the enum.
48
+ */
49
+ export interface RobotStatusContributor<TStatus = RobotHealth | RobotReadiness> {
50
+ /** Identifier of the contributor, e.g. `battery`. */
51
+ id: string;
52
+ /** The agent component that raised it, e.g. `dji`. */
53
+ component: string;
54
+ /** Human-readable explanation, e.g. `battery below 20%`; empty when none was given. */
55
+ reason: string;
56
+ /** The contributor's verdict for its axis. */
57
+ status: TStatus;
58
+ }
59
+ /**
60
+ * The per-contributor breakdown behind a robot's reported health and readiness,
61
+ * from the agent's `robot/status/contributors` topic.
62
+ */
63
+ export interface RobotStatusContributors {
64
+ health: RobotStatusContributor<RobotHealth>[];
65
+ readiness: RobotStatusContributor<RobotReadiness>[];
66
+ }
@@ -30,13 +30,22 @@ export declare enum RobotReadiness {
30
30
  BUSY = "busy"
31
31
  }
32
32
  /**
33
- * Whether the robot is connected to a single cloud service, as reported by that
34
- * service's `connections` endpoint (or, for `telemetry-liveness`, derived from
35
- * heartbeat freshness).
33
+ * Whether the robot is connected to a single cloud service reported by that
34
+ * service's `connections` endpoint, derived from heartbeat freshness (for
35
+ * `telemetry-liveness`), or supplied by a caller-injected signal
36
+ * (`RobotStatusOptions.extraServices`), the only source that can report {@link FAILED}.
36
37
  */
37
38
  export declare enum ServiceConnection {
38
39
  CONNECTED = "connected",
39
40
  DISCONNECTED = "disconnected",
41
+ /**
42
+ * The service reports an explicit fault — a stronger signal than {@link DISCONNECTED} (which just
43
+ * means "not connected", as expected of a robot that is simply off). A failed virtual-robot
44
+ * deployment is the motivating case. Any `FAILED` service forces the aggregate to
45
+ * {@link RobotConnectivity.DEGRADED}, so a fault raises attention even when nothing else is
46
+ * connected — see `aggregateConnectivity`.
47
+ */
48
+ FAILED = "failed",
40
49
  /** The service's connectivity has not yet been determined (e.g. not polled). */
41
50
  UNKNOWN = "unknown"
42
51
  }
@@ -45,10 +54,13 @@ export declare enum ServiceConnection {
45
54
  * expected services.
46
55
  */
47
56
  export declare enum RobotConnectivity {
48
- /** At least one expected service is connected and none is disconnected. */
57
+ /** At least one expected service is connected and none is disconnected or failed. */
49
58
  ONLINE = "online",
50
- /** Some, but not all, expected services report connected. */
59
+ /**
60
+ * Any service reports a fault ({@link ServiceConnection.FAILED}), or a genuine mix — some
61
+ * connected, some disconnected. A `FAILED` service forces this even when nothing is connected.
62
+ */
51
63
  DEGRADED = "degraded",
52
- /** No expected service reports connected. */
64
+ /** Nothing connected, and nothing failed. */
53
65
  OFFLINE = "offline"
54
66
  }
@@ -35,14 +35,23 @@ var RobotReadiness;
35
35
  RobotReadiness["BUSY"] = "busy";
36
36
  })(RobotReadiness || (exports.RobotReadiness = RobotReadiness = {}));
37
37
  /**
38
- * Whether the robot is connected to a single cloud service, as reported by that
39
- * service's `connections` endpoint (or, for `telemetry-liveness`, derived from
40
- * heartbeat freshness).
38
+ * Whether the robot is connected to a single cloud service reported by that
39
+ * service's `connections` endpoint, derived from heartbeat freshness (for
40
+ * `telemetry-liveness`), or supplied by a caller-injected signal
41
+ * (`RobotStatusOptions.extraServices`), the only source that can report {@link FAILED}.
41
42
  */
42
43
  var ServiceConnection;
43
44
  (function (ServiceConnection) {
44
45
  ServiceConnection["CONNECTED"] = "connected";
45
46
  ServiceConnection["DISCONNECTED"] = "disconnected";
47
+ /**
48
+ * The service reports an explicit fault — a stronger signal than {@link DISCONNECTED} (which just
49
+ * means "not connected", as expected of a robot that is simply off). A failed virtual-robot
50
+ * deployment is the motivating case. Any `FAILED` service forces the aggregate to
51
+ * {@link RobotConnectivity.DEGRADED}, so a fault raises attention even when nothing else is
52
+ * connected — see `aggregateConnectivity`.
53
+ */
54
+ ServiceConnection["FAILED"] = "failed";
46
55
  /** The service's connectivity has not yet been determined (e.g. not polled). */
47
56
  ServiceConnection["UNKNOWN"] = "unknown";
48
57
  })(ServiceConnection || (exports.ServiceConnection = ServiceConnection = {}));
@@ -52,10 +61,13 @@ var ServiceConnection;
52
61
  */
53
62
  var RobotConnectivity;
54
63
  (function (RobotConnectivity) {
55
- /** At least one expected service is connected and none is disconnected. */
64
+ /** At least one expected service is connected and none is disconnected or failed. */
56
65
  RobotConnectivity["ONLINE"] = "online";
57
- /** Some, but not all, expected services report connected. */
66
+ /**
67
+ * Any service reports a fault ({@link ServiceConnection.FAILED}), or a genuine mix — some
68
+ * connected, some disconnected. A `FAILED` service forces this even when nothing is connected.
69
+ */
58
70
  RobotConnectivity["DEGRADED"] = "degraded";
59
- /** No expected service reports connected. */
71
+ /** Nothing connected, and nothing failed. */
60
72
  RobotConnectivity["OFFLINE"] = "offline";
61
73
  })(RobotConnectivity || (exports.RobotConnectivity = RobotConnectivity = {}));
@@ -1,4 +1,4 @@
1
- import { IBaseService, IRocosSDKConfig, RobotConnectivityStatus, RobotStatus, RocosError } from '../models';
1
+ import { IBaseService, IRocosSDKConfig, RobotConnectivityStatus, RobotStatus, RobotStatusContributors, RocosError, ServiceConnection } from '../models';
2
2
  import { Observable } from 'rxjs';
3
3
  import { ReportedRobotStatus } from './robot-status/reportedStatus';
4
4
  import { BaseServiceAbstract } from './BaseServiceAbstract';
@@ -10,6 +10,23 @@ export interface RobotStatusOptions {
10
10
  heartbeatTimeoutMs?: number;
11
11
  /** How often the connections endpoints are polled (ms, floored internally). */
12
12
  connectivityPollMs?: number;
13
+ /**
14
+ * Extra per-robot connectivity signals, keyed by service id, folded into
15
+ * {@link RobotConnectivityStatus.services} and the aggregate on equal footing with the built-in
16
+ * services. Lets a caller inject a signal the SDK can't derive itself (e.g. virtual-robot
17
+ * deployment status); emit {@link ServiceConnection.FAILED} to surface a fault as `DEGRADED`.
18
+ *
19
+ * A key that collides with a built-in service id (`telemetry-liveness`, `robot-configs`,
20
+ * `robot-services`) is ignored in favour of the built-in.
21
+ *
22
+ * An error from an injected observable is contained as {@link ServiceConnection.UNKNOWN} so it
23
+ * can't tear down the overall status stream — but it is *terminal* for that signal: unlike the
24
+ * built-in HTTP poller (which recovers on its next tick), the errored observable is not
25
+ * resubscribed, so the service stays `UNKNOWN` for the life of the stream (and `getConnectivity`
26
+ * then waits out its timeout). To report a recoverable fault, emit `FAILED` / `UNKNOWN` as a
27
+ * value rather than letting the observable error.
28
+ */
29
+ extraServices?: Record<string, Observable<ServiceConnection>>;
13
30
  }
14
31
  /**
15
32
  * The single source of truth for a robot's status. It composes the reported
@@ -22,6 +39,8 @@ export declare class RobotStatusService extends BaseServiceAbstract implements I
22
39
  getStatus(): boolean;
23
40
  /** Health + readiness as reported by the robot (new topic, legacy heartbeat fallback). */
24
41
  getReportedStatusChanges(projectId: string, callsign: string): Observable<ReportedRobotStatus>;
42
+ /** Per-contributor breakdown behind the reported health and readiness, with the reason for each. */
43
+ getStatusContributorsChanges(projectId: string, callsign: string): Observable<RobotStatusContributors>;
25
44
  /** Per-service connectivity plus the aggregate verdict, polled. */
26
45
  getConnectivityChanges(projectId: string, callsign: string, opts?: RobotStatusOptions): Observable<RobotConnectivityStatus>;
27
46
  /** One-shot connectivity snapshot: resolves once every expected service is known, or after a timeout. */
@@ -42,6 +42,10 @@ class RobotStatusService extends BaseServiceAbstract_1.BaseServiceAbstract {
42
42
  getReportedStatusChanges(projectId, callsign) {
43
43
  return (0, reportedStatus_1.getReportedStatusChanges)(this.telemetryService, projectId, callsign);
44
44
  }
45
+ /** Per-contributor breakdown behind the reported health and readiness, with the reason for each. */
46
+ getStatusContributorsChanges(projectId, callsign) {
47
+ return (0, reportedStatus_1.getStatusContributorsChanges)(this.telemetryService, projectId, callsign);
48
+ }
45
49
  /** Per-service connectivity plus the aggregate verdict, polled. */
46
50
  getConnectivityChanges(projectId, callsign, opts) {
47
51
  return (0, connectivity_1.getConnectivityChanges)(this.connectivityDeps(projectId, callsign, opts), projectId, callsign, opts?.expectedServices, opts?.connectivityPollMs);
@@ -88,6 +92,7 @@ class RobotStatusService extends BaseServiceAbstract_1.BaseServiceAbstract {
88
92
  baseUrl: this.config.url,
89
93
  insecure: this.config.insecure,
90
94
  telemetryLiveness$: (0, reportedStatus_1.getTelemetryLiveness)(this.telemetryService, projectId, callsign, opts?.heartbeatTimeoutMs),
95
+ extraServices: opts?.extraServices,
91
96
  };
92
97
  }
93
98
  }
@@ -3,7 +3,6 @@ import { CallsignStatus, IRocosSDKConfig, IRocosTelemetryMessage, IStreamStatusM
3
3
  import { BaseStreamService } from './BaseStreamService';
4
4
  export declare class TelemetryService extends BaseStreamService<ITelemetryStream, ITelemetryStreamConfig> {
5
5
  private static grpcWebDeprecationWarned;
6
- private statusSubscription;
7
6
  private changeStream$;
8
7
  private streamStatusSubject$;
9
8
  constructor(config: IRocosSDKConfig);
@@ -67,7 +67,9 @@ class TelemetryService extends BaseStreamService_1.BaseStreamService {
67
67
  const { stream, isNew, ready, release } = this.acquireStream(identifier_1.IDENTIFIER_NAME_TELEMETRY, this.buildStreamConfig(params.projectId, callsignsLookup, sources, scope));
68
68
  try {
69
69
  stream.addSubscription(subscriptionParams);
70
- if (!isNew) {
70
+ // Forward the stream's status onto the per-scope subject once, at creation; it ends when the
71
+ // stream stops (statusStream$ completes), so it is not re-created or leaked across subscribers.
72
+ if (isNew) {
71
73
  stream.statusStream$.subscribe((msg) => {
72
74
  this.streamStatusSubject$.next({
73
75
  scope,
@@ -81,15 +83,8 @@ class TelemetryService extends BaseStreamService_1.BaseStreamService {
81
83
  release();
82
84
  throw e;
83
85
  }
84
- const messages$ = ready.then(() => {
85
- if (!this.statusSubscription) {
86
- this.statusSubscription = stream.statusStream$.subscribe((msg) => {
87
- this.status = msg === models_1.SubscriberStatusEnum.STOPPED || msg === models_1.SubscriberStatusEnum.ALIVE;
88
- this.status$.next(msg);
89
- });
90
- }
91
- return (0, rxjs_1.from)(stream.messageStream$);
92
- });
86
+ // status$ / getStatus() are driven by the acquireStream factory (BaseStreamService), not here.
87
+ const messages$ = ready.then(() => (0, rxjs_1.from)(stream.messageStream$));
93
88
  return (0, rxjs_1.from)(messages$).pipe((0, rxjs_1.mergeAll)(), (0, operators_1.filter)((message) => {
94
89
  return callsignsLookup.includesCallsign(message.callsign) && sources.includes(message.source);
95
90
  }), (0, operators_1.finalize)(() => {
@@ -36,18 +36,19 @@ export declare const buildConnectionsUrl: (serviceId: string, baseUrl: string, p
36
36
  export declare const mapConnectionStatus: (connection?: IConnectedCallsign | null) => ServiceConnection;
37
37
  /**
38
38
  * Aggregates per-service connection states into an overall verdict:
39
- * - `ONLINE` at least one service connected and none disconnected.
40
- * - `DEGRADED` — a genuine mix: some connected, some disconnected.
39
+ * - `DEGRADED` any service reports an explicit `FAILED` fault, or a genuine mix (some connected,
40
+ * some disconnected).
41
+ * - `ONLINE` — at least one service connected and none disconnected or failed.
41
42
  * - `OFFLINE` — nothing connected.
42
43
  *
43
- * `UNKNOWN` services (e.g. not yet polled) never force `DEGRADED`, so startup
44
- * settles on `OFFLINE` (gray) rather than a spurious flashing-red degraded state.
44
+ * A `FAILED` service forces `DEGRADED` even when nothing else is connected, so a fault (e.g. a failed
45
+ * deployment) raises attention rather than reading as a plain, switched-off `OFFLINE`.
45
46
  *
46
- * Note: this optimism also means a service held `UNKNOWN` indefinitely (e.g. an
47
- * endpoint whose polls keep failing) does not by itself pull the aggregate below
48
- * `ONLINE` a deliberate v1 tradeoff to keep transient errors from flapping the
49
- * indicator. Surfacing a long-lived failure distinctly (likely via health rather
50
- * than connectivity) is left as a future refinement.
47
+ * `UNKNOWN` services (e.g. not yet polled) never force `DEGRADED`, so startup settles on `OFFLINE`
48
+ * (gray) rather than a spurious flashing-red degraded state. This optimism also means a service held
49
+ * `UNKNOWN` indefinitely (e.g. an endpoint whose polls keep failing) does not by itself pull the
50
+ * aggregate below `ONLINE` — a deliberate tradeoff to keep transient errors from flapping the
51
+ * indicator; a service that wants to be surfaced reports `FAILED` explicitly.
51
52
  */
52
53
  export declare const aggregateConnectivity: (services: Record<string, ServiceConnection>) => RobotConnectivity;
53
54
  /**
@@ -66,11 +67,14 @@ export interface ConnectivityDeps {
66
67
  insecure?: boolean;
67
68
  /** The telemetry-liveness signal (from `getTelemetryLiveness`), folded in as one service. */
68
69
  telemetryLiveness$: Observable<ServiceConnection>;
70
+ /** Caller-injected connectivity signals, keyed by service id, folded in on equal footing. */
71
+ extraServices?: Record<string, Observable<ServiceConnection>>;
69
72
  }
70
73
  /**
71
74
  * Emits the robot's {@link RobotConnectivityStatus} — the per-service breakdown
72
- * plus the aggregate verdict — for the configured `expectedServices`. HTTP
73
- * services are polled; `telemetry-liveness` uses the supplied observable; any
74
- * unrecognised service id resolves to `UNKNOWN`.
75
+ * plus the aggregate verdict — for the configured `expectedServices`, plus any
76
+ * caller-injected `extraServices` (folded in on equal footing). HTTP services
77
+ * are polled; `telemetry-liveness` and injected services use their supplied
78
+ * observable; any unrecognised service id resolves to `UNKNOWN`.
75
79
  */
76
80
  export declare const getConnectivityChanges: (deps: ConnectivityDeps, projectId: string, callsign: string, expectedServices?: string[], pollMs?: number) => Observable<RobotConnectivityStatus>;
@@ -58,21 +58,24 @@ const mapConnectionStatus = (connection) => {
58
58
  exports.mapConnectionStatus = mapConnectionStatus;
59
59
  /**
60
60
  * Aggregates per-service connection states into an overall verdict:
61
- * - `ONLINE` at least one service connected and none disconnected.
62
- * - `DEGRADED` — a genuine mix: some connected, some disconnected.
61
+ * - `DEGRADED` any service reports an explicit `FAILED` fault, or a genuine mix (some connected,
62
+ * some disconnected).
63
+ * - `ONLINE` — at least one service connected and none disconnected or failed.
63
64
  * - `OFFLINE` — nothing connected.
64
65
  *
65
- * `UNKNOWN` services (e.g. not yet polled) never force `DEGRADED`, so startup
66
- * settles on `OFFLINE` (gray) rather than a spurious flashing-red degraded state.
66
+ * A `FAILED` service forces `DEGRADED` even when nothing else is connected, so a fault (e.g. a failed
67
+ * deployment) raises attention rather than reading as a plain, switched-off `OFFLINE`.
67
68
  *
68
- * Note: this optimism also means a service held `UNKNOWN` indefinitely (e.g. an
69
- * endpoint whose polls keep failing) does not by itself pull the aggregate below
70
- * `ONLINE` a deliberate v1 tradeoff to keep transient errors from flapping the
71
- * indicator. Surfacing a long-lived failure distinctly (likely via health rather
72
- * than connectivity) is left as a future refinement.
69
+ * `UNKNOWN` services (e.g. not yet polled) never force `DEGRADED`, so startup settles on `OFFLINE`
70
+ * (gray) rather than a spurious flashing-red degraded state. This optimism also means a service held
71
+ * `UNKNOWN` indefinitely (e.g. an endpoint whose polls keep failing) does not by itself pull the
72
+ * aggregate below `ONLINE` — a deliberate tradeoff to keep transient errors from flapping the
73
+ * indicator; a service that wants to be surfaced reports `FAILED` explicitly.
73
74
  */
74
75
  const aggregateConnectivity = (services) => {
75
76
  const values = Object.values(services);
77
+ if (values.includes(models_1.ServiceConnection.FAILED))
78
+ return models_1.RobotConnectivity.DEGRADED;
76
79
  const hasConnected = values.includes(models_1.ServiceConnection.CONNECTED);
77
80
  const hasDisconnected = values.includes(models_1.ServiceConnection.DISCONNECTED);
78
81
  if (hasConnected && hasDisconnected)
@@ -107,24 +110,32 @@ const connectivityEquals = (a, b) => {
107
110
  };
108
111
  /**
109
112
  * Emits the robot's {@link RobotConnectivityStatus} — the per-service breakdown
110
- * plus the aggregate verdict — for the configured `expectedServices`. HTTP
111
- * services are polled; `telemetry-liveness` uses the supplied observable; any
112
- * unrecognised service id resolves to `UNKNOWN`.
113
+ * plus the aggregate verdict — for the configured `expectedServices`, plus any
114
+ * caller-injected `extraServices` (folded in on equal footing). HTTP services
115
+ * are polled; `telemetry-liveness` and injected services use their supplied
116
+ * observable; any unrecognised service id resolves to `UNKNOWN`.
113
117
  */
114
118
  const getConnectivityChanges = (deps, projectId, callsign, expectedServices = exports.DEFAULT_EXPECTED_SERVICES, pollMs = DEFAULT_CONNECTIVITY_POLL_MS) => {
119
+ const extraServices = deps.extraServices ?? {};
120
+ const serviceIds = [...expectedServices, ...Object.keys(extraServices).filter((id) => !expectedServices.includes(id))];
115
121
  // combineLatest([]) completes without emitting; emit an explicit empty/offline status instead.
116
- if (expectedServices.length === 0) {
122
+ if (serviceIds.length === 0) {
117
123
  return (0, rxjs_1.of)({ overall: models_1.RobotConnectivity.OFFLINE, services: {} });
118
124
  }
119
- const perService = expectedServices.map((serviceId) => {
125
+ const perService = serviceIds.map((serviceId) => {
120
126
  let source$;
121
127
  if (serviceId === exports.TELEMETRY_LIVENESS_SERVICE) {
122
128
  source$ = deps.telemetryLiveness$;
123
129
  }
124
- else if (serviceId in exports.CONNECTION_SERVICE_PREFIXES) {
130
+ else if (Object.prototype.hasOwnProperty.call(exports.CONNECTION_SERVICE_PREFIXES, serviceId)) {
125
131
  const url = (0, exports.buildConnectionsUrl)(serviceId, deps.baseUrl, projectId, callsign, deps.insecure);
126
132
  source$ = (0, exports.getServiceConnectionChanges)(deps.httpGet, url, pollMs);
127
133
  }
134
+ else if (Object.prototype.hasOwnProperty.call(extraServices, serviceId)) {
135
+ // Contain an injected observable's errors as UNKNOWN, mirroring the built-in sources, so a
136
+ // faulty caller signal can't error out combineLatest and tear down the whole aggregate.
137
+ source$ = extraServices[serviceId].pipe((0, rxjs_1.catchError)(() => (0, rxjs_1.of)(models_1.ServiceConnection.UNKNOWN)));
138
+ }
128
139
  else {
129
140
  source$ = (0, rxjs_1.of)(models_1.ServiceConnection.UNKNOWN);
130
141
  }
@@ -1,10 +1,12 @@
1
1
  import { Observable } from 'rxjs';
2
- import { RobotHealth, RobotReadiness, ServiceConnection } from '../../models';
2
+ import { RobotHealth, RobotReadiness, RobotStatusContributors, ServiceConnection } from '../../models';
3
3
  import { TelemetryService } from '../TelemetryService';
4
4
  /** The new agent topic carrying health + readiness (replaces the heartbeat). */
5
5
  export declare const ROBOT_STATUS_SOURCE = "/diagnostics/robot/status";
6
6
  /** The legacy heartbeat topic — liveness only, no status payload. */
7
7
  export declare const LEGACY_HEARTBEAT_SOURCE = "/rocos/agent/telemetry/heartbeat";
8
+ /** The per-contributor breakdown behind the reported status (health + readiness). */
9
+ export declare const ROBOT_STATUS_CONTRIBUTORS_SOURCE = "/diagnostics/robot/status/contributors";
8
10
  export interface ReportedRobotStatus {
9
11
  health: RobotHealth;
10
12
  readiness: RobotReadiness;
@@ -28,6 +30,17 @@ export declare const parseRobotReadiness: (value: unknown) => RobotReadiness;
28
30
  * new topic.
29
31
  */
30
32
  export declare const getReportedStatusChanges: (telemetry: TelemetryService, projectId: string, callsign: string) => Observable<ReportedRobotStatus>;
33
+ /**
34
+ * Emits the per-contributor breakdown behind the robot's reported health and
35
+ * readiness, from the agent's `robot/status/contributors` topic. Each
36
+ * contributor's integer wire value is mapped onto {@link RobotHealth} /
37
+ * {@link RobotReadiness}; a missing `health`/`readiness` array becomes `[]`.
38
+ *
39
+ * Starts with an empty breakdown so consumers combining this with other streams
40
+ * emit immediately, and — like {@link getReportedStatusChanges} — a telemetry
41
+ * error falls back to an empty breakdown rather than terminating the stream.
42
+ */
43
+ export declare const getStatusContributorsChanges: (telemetry: TelemetryService, projectId: string, callsign: string) => Observable<RobotStatusContributors>;
31
44
  /**
32
45
  * Emits the `telemetry-liveness` connectivity signal: whether a heartbeat (from
33
46
  * either the new `robot/status` topic or the legacy heartbeat) has been received
@@ -1,12 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getTelemetryLiveness = exports.getReportedStatusChanges = exports.parseRobotReadiness = exports.parseRobotHealth = exports.LEGACY_HEARTBEAT_SOURCE = exports.ROBOT_STATUS_SOURCE = void 0;
3
+ exports.getTelemetryLiveness = exports.getStatusContributorsChanges = exports.getReportedStatusChanges = exports.parseRobotReadiness = exports.parseRobotHealth = exports.ROBOT_STATUS_CONTRIBUTORS_SOURCE = exports.LEGACY_HEARTBEAT_SOURCE = exports.ROBOT_STATUS_SOURCE = void 0;
4
4
  const rxjs_1 = require("rxjs");
5
5
  const models_1 = require("../../models");
6
6
  /** The new agent topic carrying health + readiness (replaces the heartbeat). */
7
7
  exports.ROBOT_STATUS_SOURCE = '/diagnostics/robot/status';
8
8
  /** The legacy heartbeat topic — liveness only, no status payload. */
9
9
  exports.LEGACY_HEARTBEAT_SOURCE = '/rocos/agent/telemetry/heartbeat';
10
+ /** The per-contributor breakdown behind the reported status (health + readiness). */
11
+ exports.ROBOT_STATUS_CONTRIBUTORS_SOURCE = `${exports.ROBOT_STATUS_SOURCE}/contributors`;
10
12
  const DEFAULT_HEARTBEAT_TIMEOUT_MS = 5000;
11
13
  const DEFAULT_INTERVAL_MS = 2000;
12
14
  /**
@@ -84,6 +86,39 @@ const getReportedStatusChanges = (telemetry, projectId, callsign) => {
84
86
  (0, rxjs_1.catchError)(() => (0, rxjs_1.of)({ health: models_1.RobotHealth.UNKNOWN, readiness: models_1.RobotReadiness.UNKNOWN })));
85
87
  };
86
88
  exports.getReportedStatusChanges = getReportedStatusChanges;
89
+ const toContributor = (raw, parse) => ({
90
+ id: raw.id ?? '',
91
+ component: raw.component ?? '',
92
+ reason: raw.reason ?? '',
93
+ status: parse(raw.value),
94
+ });
95
+ const EMPTY_CONTRIBUTORS = { health: [], readiness: [] };
96
+ /**
97
+ * Emits the per-contributor breakdown behind the robot's reported health and
98
+ * readiness, from the agent's `robot/status/contributors` topic. Each
99
+ * contributor's integer wire value is mapped onto {@link RobotHealth} /
100
+ * {@link RobotReadiness}; a missing `health`/`readiness` array becomes `[]`.
101
+ *
102
+ * Starts with an empty breakdown so consumers combining this with other streams
103
+ * emit immediately, and — like {@link getReportedStatusChanges} — a telemetry
104
+ * error falls back to an empty breakdown rather than terminating the stream.
105
+ */
106
+ const getStatusContributorsChanges = (telemetry, projectId, callsign) => {
107
+ return telemetry
108
+ .subscribe({
109
+ projectId,
110
+ callsigns: [callsign],
111
+ sources: [exports.ROBOT_STATUS_CONTRIBUTORS_SOURCE],
112
+ })
113
+ .pipe((0, rxjs_1.map)((message) => {
114
+ const payload = message.payload ?? {};
115
+ return {
116
+ health: (payload.health ?? []).map((c) => toContributor(c, exports.parseRobotHealth)),
117
+ readiness: (payload.readiness ?? []).map((c) => toContributor(c, exports.parseRobotReadiness)),
118
+ };
119
+ }), (0, rxjs_1.startWith)(EMPTY_CONTRIBUTORS), (0, rxjs_1.distinctUntilChanged)((a, b) => JSON.stringify(a) === JSON.stringify(b)), (0, rxjs_1.catchError)(() => (0, rxjs_1.of)(EMPTY_CONTRIBUTORS)));
120
+ };
121
+ exports.getStatusContributorsChanges = getStatusContributorsChanges;
87
122
  /**
88
123
  * Emits the `telemetry-liveness` connectivity signal: whether a heartbeat (from
89
124
  * either the new `robot/status` topic or the legacy heartbeat) has been received
@@ -59,6 +59,10 @@ export class TelemetryStreamAbstract {
59
59
  }
60
60
  this.subscriberStatus = SubscriberStatusEnum.STOPPED;
61
61
  this.statusStream$.next(this.subscriberStatus);
62
+ // The stream is being torn down (the registry only stops a stream when it is evicted), so
63
+ // complete the status stream: this releases every service-level forwarder subscribed to it
64
+ // rather than leaving them attached to a dead instance.
65
+ this.statusStream$.complete();
62
66
  }
63
67
  addSubscription(params) {
64
68
  this.logger.info('Adding subscriptions from stream', params.uniqueId);
@@ -41,3 +41,26 @@ export interface RobotStatus {
41
41
  connectivity: RobotConnectivityStatus;
42
42
  colour: RobotStatusColour;
43
43
  }
44
+ /**
45
+ * One contributor to a reported-status axis, from the agent's
46
+ * `robot/status/contributors` topic. `status` is the contributor's own verdict
47
+ * for that axis — the integer wire value mapped onto the enum.
48
+ */
49
+ export interface RobotStatusContributor<TStatus = RobotHealth | RobotReadiness> {
50
+ /** Identifier of the contributor, e.g. `battery`. */
51
+ id: string;
52
+ /** The agent component that raised it, e.g. `dji`. */
53
+ component: string;
54
+ /** Human-readable explanation, e.g. `battery below 20%`; empty when none was given. */
55
+ reason: string;
56
+ /** The contributor's verdict for its axis. */
57
+ status: TStatus;
58
+ }
59
+ /**
60
+ * The per-contributor breakdown behind a robot's reported health and readiness,
61
+ * from the agent's `robot/status/contributors` topic.
62
+ */
63
+ export interface RobotStatusContributors {
64
+ health: RobotStatusContributor<RobotHealth>[];
65
+ readiness: RobotStatusContributor<RobotReadiness>[];
66
+ }
@@ -30,13 +30,22 @@ export declare enum RobotReadiness {
30
30
  BUSY = "busy"
31
31
  }
32
32
  /**
33
- * Whether the robot is connected to a single cloud service, as reported by that
34
- * service's `connections` endpoint (or, for `telemetry-liveness`, derived from
35
- * heartbeat freshness).
33
+ * Whether the robot is connected to a single cloud service reported by that
34
+ * service's `connections` endpoint, derived from heartbeat freshness (for
35
+ * `telemetry-liveness`), or supplied by a caller-injected signal
36
+ * (`RobotStatusOptions.extraServices`), the only source that can report {@link FAILED}.
36
37
  */
37
38
  export declare enum ServiceConnection {
38
39
  CONNECTED = "connected",
39
40
  DISCONNECTED = "disconnected",
41
+ /**
42
+ * The service reports an explicit fault — a stronger signal than {@link DISCONNECTED} (which just
43
+ * means "not connected", as expected of a robot that is simply off). A failed virtual-robot
44
+ * deployment is the motivating case. Any `FAILED` service forces the aggregate to
45
+ * {@link RobotConnectivity.DEGRADED}, so a fault raises attention even when nothing else is
46
+ * connected — see `aggregateConnectivity`.
47
+ */
48
+ FAILED = "failed",
40
49
  /** The service's connectivity has not yet been determined (e.g. not polled). */
41
50
  UNKNOWN = "unknown"
42
51
  }
@@ -45,10 +54,13 @@ export declare enum ServiceConnection {
45
54
  * expected services.
46
55
  */
47
56
  export declare enum RobotConnectivity {
48
- /** At least one expected service is connected and none is disconnected. */
57
+ /** At least one expected service is connected and none is disconnected or failed. */
49
58
  ONLINE = "online",
50
- /** Some, but not all, expected services report connected. */
59
+ /**
60
+ * Any service reports a fault ({@link ServiceConnection.FAILED}), or a genuine mix — some
61
+ * connected, some disconnected. A `FAILED` service forces this even when nothing is connected.
62
+ */
51
63
  DEGRADED = "degraded",
52
- /** No expected service reports connected. */
64
+ /** Nothing connected, and nothing failed. */
53
65
  OFFLINE = "offline"
54
66
  }
@@ -32,14 +32,23 @@ export var RobotReadiness;
32
32
  RobotReadiness["BUSY"] = "busy";
33
33
  })(RobotReadiness || (RobotReadiness = {}));
34
34
  /**
35
- * Whether the robot is connected to a single cloud service, as reported by that
36
- * service's `connections` endpoint (or, for `telemetry-liveness`, derived from
37
- * heartbeat freshness).
35
+ * Whether the robot is connected to a single cloud service reported by that
36
+ * service's `connections` endpoint, derived from heartbeat freshness (for
37
+ * `telemetry-liveness`), or supplied by a caller-injected signal
38
+ * (`RobotStatusOptions.extraServices`), the only source that can report {@link FAILED}.
38
39
  */
39
40
  export var ServiceConnection;
40
41
  (function (ServiceConnection) {
41
42
  ServiceConnection["CONNECTED"] = "connected";
42
43
  ServiceConnection["DISCONNECTED"] = "disconnected";
44
+ /**
45
+ * The service reports an explicit fault — a stronger signal than {@link DISCONNECTED} (which just
46
+ * means "not connected", as expected of a robot that is simply off). A failed virtual-robot
47
+ * deployment is the motivating case. Any `FAILED` service forces the aggregate to
48
+ * {@link RobotConnectivity.DEGRADED}, so a fault raises attention even when nothing else is
49
+ * connected — see `aggregateConnectivity`.
50
+ */
51
+ ServiceConnection["FAILED"] = "failed";
43
52
  /** The service's connectivity has not yet been determined (e.g. not polled). */
44
53
  ServiceConnection["UNKNOWN"] = "unknown";
45
54
  })(ServiceConnection || (ServiceConnection = {}));
@@ -49,10 +58,13 @@ export var ServiceConnection;
49
58
  */
50
59
  export var RobotConnectivity;
51
60
  (function (RobotConnectivity) {
52
- /** At least one expected service is connected and none is disconnected. */
61
+ /** At least one expected service is connected and none is disconnected or failed. */
53
62
  RobotConnectivity["ONLINE"] = "online";
54
- /** Some, but not all, expected services report connected. */
63
+ /**
64
+ * Any service reports a fault ({@link ServiceConnection.FAILED}), or a genuine mix — some
65
+ * connected, some disconnected. A `FAILED` service forces this even when nothing is connected.
66
+ */
55
67
  RobotConnectivity["DEGRADED"] = "degraded";
56
- /** No expected service reports connected. */
68
+ /** Nothing connected, and nothing failed. */
57
69
  RobotConnectivity["OFFLINE"] = "offline";
58
70
  })(RobotConnectivity || (RobotConnectivity = {}));
@@ -1,4 +1,4 @@
1
- import { IBaseService, IRocosSDKConfig, RobotConnectivityStatus, RobotStatus, RocosError } from '../models';
1
+ import { IBaseService, IRocosSDKConfig, RobotConnectivityStatus, RobotStatus, RobotStatusContributors, RocosError, ServiceConnection } from '../models';
2
2
  import { Observable } from 'rxjs';
3
3
  import { ReportedRobotStatus } from './robot-status/reportedStatus';
4
4
  import { BaseServiceAbstract } from './BaseServiceAbstract';
@@ -10,6 +10,23 @@ export interface RobotStatusOptions {
10
10
  heartbeatTimeoutMs?: number;
11
11
  /** How often the connections endpoints are polled (ms, floored internally). */
12
12
  connectivityPollMs?: number;
13
+ /**
14
+ * Extra per-robot connectivity signals, keyed by service id, folded into
15
+ * {@link RobotConnectivityStatus.services} and the aggregate on equal footing with the built-in
16
+ * services. Lets a caller inject a signal the SDK can't derive itself (e.g. virtual-robot
17
+ * deployment status); emit {@link ServiceConnection.FAILED} to surface a fault as `DEGRADED`.
18
+ *
19
+ * A key that collides with a built-in service id (`telemetry-liveness`, `robot-configs`,
20
+ * `robot-services`) is ignored in favour of the built-in.
21
+ *
22
+ * An error from an injected observable is contained as {@link ServiceConnection.UNKNOWN} so it
23
+ * can't tear down the overall status stream — but it is *terminal* for that signal: unlike the
24
+ * built-in HTTP poller (which recovers on its next tick), the errored observable is not
25
+ * resubscribed, so the service stays `UNKNOWN` for the life of the stream (and `getConnectivity`
26
+ * then waits out its timeout). To report a recoverable fault, emit `FAILED` / `UNKNOWN` as a
27
+ * value rather than letting the observable error.
28
+ */
29
+ extraServices?: Record<string, Observable<ServiceConnection>>;
13
30
  }
14
31
  /**
15
32
  * The single source of truth for a robot's status. It composes the reported
@@ -22,6 +39,8 @@ export declare class RobotStatusService extends BaseServiceAbstract implements I
22
39
  getStatus(): boolean;
23
40
  /** Health + readiness as reported by the robot (new topic, legacy heartbeat fallback). */
24
41
  getReportedStatusChanges(projectId: string, callsign: string): Observable<ReportedRobotStatus>;
42
+ /** Per-contributor breakdown behind the reported health and readiness, with the reason for each. */
43
+ getStatusContributorsChanges(projectId: string, callsign: string): Observable<RobotStatusContributors>;
25
44
  /** Per-service connectivity plus the aggregate verdict, polled. */
26
45
  getConnectivityChanges(projectId: string, callsign: string, opts?: RobotStatusOptions): Observable<RobotConnectivityStatus>;
27
46
  /** One-shot connectivity snapshot: resolves once every expected service is known, or after a timeout. */
@@ -1,7 +1,7 @@
1
1
  import { getConnectivityChanges } from './robot-status/connectivity';
2
2
  import { RobotHealth, RobotReadiness, RocosError, ServiceConnection, errorCodes, } from '../models';
3
3
  import { combineLatest, distinctUntilChanged, first, firstValueFrom, map, of, shareReplay, startWith, take, tap, timeout, } from 'rxjs';
4
- import { getReportedStatusChanges, getTelemetryLiveness } from './robot-status/reportedStatus';
4
+ import { getReportedStatusChanges, getStatusContributorsChanges, getTelemetryLiveness, } from './robot-status/reportedStatus';
5
5
  import { BaseServiceAbstract } from './BaseServiceAbstract';
6
6
  import { RocosLogger } from '../logger/RocosLogger';
7
7
  import { RocosStore } from '../store/RocosStore';
@@ -39,6 +39,10 @@ export class RobotStatusService extends BaseServiceAbstract {
39
39
  getReportedStatusChanges(projectId, callsign) {
40
40
  return getReportedStatusChanges(this.telemetryService, projectId, callsign);
41
41
  }
42
+ /** Per-contributor breakdown behind the reported health and readiness, with the reason for each. */
43
+ getStatusContributorsChanges(projectId, callsign) {
44
+ return getStatusContributorsChanges(this.telemetryService, projectId, callsign);
45
+ }
42
46
  /** Per-service connectivity plus the aggregate verdict, polled. */
43
47
  getConnectivityChanges(projectId, callsign, opts) {
44
48
  return getConnectivityChanges(this.connectivityDeps(projectId, callsign, opts), projectId, callsign, opts?.expectedServices, opts?.connectivityPollMs);
@@ -85,6 +89,7 @@ export class RobotStatusService extends BaseServiceAbstract {
85
89
  baseUrl: this.config.url,
86
90
  insecure: this.config.insecure,
87
91
  telemetryLiveness$: getTelemetryLiveness(this.telemetryService, projectId, callsign, opts?.heartbeatTimeoutMs),
92
+ extraServices: opts?.extraServices,
88
93
  };
89
94
  }
90
95
  }
@@ -3,7 +3,6 @@ import { CallsignStatus, IRocosSDKConfig, IRocosTelemetryMessage, IStreamStatusM
3
3
  import { BaseStreamService } from './BaseStreamService';
4
4
  export declare class TelemetryService extends BaseStreamService<ITelemetryStream, ITelemetryStreamConfig> {
5
5
  private static grpcWebDeprecationWarned;
6
- private statusSubscription;
7
6
  private changeStream$;
8
7
  private streamStatusSubject$;
9
8
  constructor(config: IRocosSDKConfig);
@@ -64,7 +64,9 @@ export class TelemetryService extends BaseStreamService {
64
64
  const { stream, isNew, ready, release } = this.acquireStream(IDENTIFIER_NAME_TELEMETRY, this.buildStreamConfig(params.projectId, callsignsLookup, sources, scope));
65
65
  try {
66
66
  stream.addSubscription(subscriptionParams);
67
- if (!isNew) {
67
+ // Forward the stream's status onto the per-scope subject once, at creation; it ends when the
68
+ // stream stops (statusStream$ completes), so it is not re-created or leaked across subscribers.
69
+ if (isNew) {
68
70
  stream.statusStream$.subscribe((msg) => {
69
71
  this.streamStatusSubject$.next({
70
72
  scope,
@@ -78,15 +80,8 @@ export class TelemetryService extends BaseStreamService {
78
80
  release();
79
81
  throw e;
80
82
  }
81
- const messages$ = ready.then(() => {
82
- if (!this.statusSubscription) {
83
- this.statusSubscription = stream.statusStream$.subscribe((msg) => {
84
- this.status = msg === SubscriberStatusEnum.STOPPED || msg === SubscriberStatusEnum.ALIVE;
85
- this.status$.next(msg);
86
- });
87
- }
88
- return from(stream.messageStream$);
89
- });
83
+ // status$ / getStatus() are driven by the acquireStream factory (BaseStreamService), not here.
84
+ const messages$ = ready.then(() => from(stream.messageStream$));
90
85
  return from(messages$).pipe(mergeAll(), filter((message) => {
91
86
  return callsignsLookup.includesCallsign(message.callsign) && sources.includes(message.source);
92
87
  }), finalize(() => {
@@ -36,18 +36,19 @@ export declare const buildConnectionsUrl: (serviceId: string, baseUrl: string, p
36
36
  export declare const mapConnectionStatus: (connection?: IConnectedCallsign | null) => ServiceConnection;
37
37
  /**
38
38
  * Aggregates per-service connection states into an overall verdict:
39
- * - `ONLINE` at least one service connected and none disconnected.
40
- * - `DEGRADED` — a genuine mix: some connected, some disconnected.
39
+ * - `DEGRADED` any service reports an explicit `FAILED` fault, or a genuine mix (some connected,
40
+ * some disconnected).
41
+ * - `ONLINE` — at least one service connected and none disconnected or failed.
41
42
  * - `OFFLINE` — nothing connected.
42
43
  *
43
- * `UNKNOWN` services (e.g. not yet polled) never force `DEGRADED`, so startup
44
- * settles on `OFFLINE` (gray) rather than a spurious flashing-red degraded state.
44
+ * A `FAILED` service forces `DEGRADED` even when nothing else is connected, so a fault (e.g. a failed
45
+ * deployment) raises attention rather than reading as a plain, switched-off `OFFLINE`.
45
46
  *
46
- * Note: this optimism also means a service held `UNKNOWN` indefinitely (e.g. an
47
- * endpoint whose polls keep failing) does not by itself pull the aggregate below
48
- * `ONLINE` a deliberate v1 tradeoff to keep transient errors from flapping the
49
- * indicator. Surfacing a long-lived failure distinctly (likely via health rather
50
- * than connectivity) is left as a future refinement.
47
+ * `UNKNOWN` services (e.g. not yet polled) never force `DEGRADED`, so startup settles on `OFFLINE`
48
+ * (gray) rather than a spurious flashing-red degraded state. This optimism also means a service held
49
+ * `UNKNOWN` indefinitely (e.g. an endpoint whose polls keep failing) does not by itself pull the
50
+ * aggregate below `ONLINE` — a deliberate tradeoff to keep transient errors from flapping the
51
+ * indicator; a service that wants to be surfaced reports `FAILED` explicitly.
51
52
  */
52
53
  export declare const aggregateConnectivity: (services: Record<string, ServiceConnection>) => RobotConnectivity;
53
54
  /**
@@ -66,11 +67,14 @@ export interface ConnectivityDeps {
66
67
  insecure?: boolean;
67
68
  /** The telemetry-liveness signal (from `getTelemetryLiveness`), folded in as one service. */
68
69
  telemetryLiveness$: Observable<ServiceConnection>;
70
+ /** Caller-injected connectivity signals, keyed by service id, folded in on equal footing. */
71
+ extraServices?: Record<string, Observable<ServiceConnection>>;
69
72
  }
70
73
  /**
71
74
  * Emits the robot's {@link RobotConnectivityStatus} — the per-service breakdown
72
- * plus the aggregate verdict — for the configured `expectedServices`. HTTP
73
- * services are polled; `telemetry-liveness` uses the supplied observable; any
74
- * unrecognised service id resolves to `UNKNOWN`.
75
+ * plus the aggregate verdict — for the configured `expectedServices`, plus any
76
+ * caller-injected `extraServices` (folded in on equal footing). HTTP services
77
+ * are polled; `telemetry-liveness` and injected services use their supplied
78
+ * observable; any unrecognised service id resolves to `UNKNOWN`.
75
79
  */
76
80
  export declare const getConnectivityChanges: (deps: ConnectivityDeps, projectId: string, callsign: string, expectedServices?: string[], pollMs?: number) => Observable<RobotConnectivityStatus>;
@@ -52,21 +52,24 @@ export const mapConnectionStatus = (connection) => {
52
52
  };
53
53
  /**
54
54
  * Aggregates per-service connection states into an overall verdict:
55
- * - `ONLINE` at least one service connected and none disconnected.
56
- * - `DEGRADED` — a genuine mix: some connected, some disconnected.
55
+ * - `DEGRADED` any service reports an explicit `FAILED` fault, or a genuine mix (some connected,
56
+ * some disconnected).
57
+ * - `ONLINE` — at least one service connected and none disconnected or failed.
57
58
  * - `OFFLINE` — nothing connected.
58
59
  *
59
- * `UNKNOWN` services (e.g. not yet polled) never force `DEGRADED`, so startup
60
- * settles on `OFFLINE` (gray) rather than a spurious flashing-red degraded state.
60
+ * A `FAILED` service forces `DEGRADED` even when nothing else is connected, so a fault (e.g. a failed
61
+ * deployment) raises attention rather than reading as a plain, switched-off `OFFLINE`.
61
62
  *
62
- * Note: this optimism also means a service held `UNKNOWN` indefinitely (e.g. an
63
- * endpoint whose polls keep failing) does not by itself pull the aggregate below
64
- * `ONLINE` a deliberate v1 tradeoff to keep transient errors from flapping the
65
- * indicator. Surfacing a long-lived failure distinctly (likely via health rather
66
- * than connectivity) is left as a future refinement.
63
+ * `UNKNOWN` services (e.g. not yet polled) never force `DEGRADED`, so startup settles on `OFFLINE`
64
+ * (gray) rather than a spurious flashing-red degraded state. This optimism also means a service held
65
+ * `UNKNOWN` indefinitely (e.g. an endpoint whose polls keep failing) does not by itself pull the
66
+ * aggregate below `ONLINE` — a deliberate tradeoff to keep transient errors from flapping the
67
+ * indicator; a service that wants to be surfaced reports `FAILED` explicitly.
67
68
  */
68
69
  export const aggregateConnectivity = (services) => {
69
70
  const values = Object.values(services);
71
+ if (values.includes(ServiceConnection.FAILED))
72
+ return RobotConnectivity.DEGRADED;
70
73
  const hasConnected = values.includes(ServiceConnection.CONNECTED);
71
74
  const hasDisconnected = values.includes(ServiceConnection.DISCONNECTED);
72
75
  if (hasConnected && hasDisconnected)
@@ -99,24 +102,32 @@ const connectivityEquals = (a, b) => {
99
102
  };
100
103
  /**
101
104
  * Emits the robot's {@link RobotConnectivityStatus} — the per-service breakdown
102
- * plus the aggregate verdict — for the configured `expectedServices`. HTTP
103
- * services are polled; `telemetry-liveness` uses the supplied observable; any
104
- * unrecognised service id resolves to `UNKNOWN`.
105
+ * plus the aggregate verdict — for the configured `expectedServices`, plus any
106
+ * caller-injected `extraServices` (folded in on equal footing). HTTP services
107
+ * are polled; `telemetry-liveness` and injected services use their supplied
108
+ * observable; any unrecognised service id resolves to `UNKNOWN`.
105
109
  */
106
110
  export const getConnectivityChanges = (deps, projectId, callsign, expectedServices = DEFAULT_EXPECTED_SERVICES, pollMs = DEFAULT_CONNECTIVITY_POLL_MS) => {
111
+ const extraServices = deps.extraServices ?? {};
112
+ const serviceIds = [...expectedServices, ...Object.keys(extraServices).filter((id) => !expectedServices.includes(id))];
107
113
  // combineLatest([]) completes without emitting; emit an explicit empty/offline status instead.
108
- if (expectedServices.length === 0) {
114
+ if (serviceIds.length === 0) {
109
115
  return of({ overall: RobotConnectivity.OFFLINE, services: {} });
110
116
  }
111
- const perService = expectedServices.map((serviceId) => {
117
+ const perService = serviceIds.map((serviceId) => {
112
118
  let source$;
113
119
  if (serviceId === TELEMETRY_LIVENESS_SERVICE) {
114
120
  source$ = deps.telemetryLiveness$;
115
121
  }
116
- else if (serviceId in CONNECTION_SERVICE_PREFIXES) {
122
+ else if (Object.prototype.hasOwnProperty.call(CONNECTION_SERVICE_PREFIXES, serviceId)) {
117
123
  const url = buildConnectionsUrl(serviceId, deps.baseUrl, projectId, callsign, deps.insecure);
118
124
  source$ = getServiceConnectionChanges(deps.httpGet, url, pollMs);
119
125
  }
126
+ else if (Object.prototype.hasOwnProperty.call(extraServices, serviceId)) {
127
+ // Contain an injected observable's errors as UNKNOWN, mirroring the built-in sources, so a
128
+ // faulty caller signal can't error out combineLatest and tear down the whole aggregate.
129
+ source$ = extraServices[serviceId].pipe(catchError(() => of(ServiceConnection.UNKNOWN)));
130
+ }
120
131
  else {
121
132
  source$ = of(ServiceConnection.UNKNOWN);
122
133
  }
@@ -1,10 +1,12 @@
1
1
  import { Observable } from 'rxjs';
2
- import { RobotHealth, RobotReadiness, ServiceConnection } from '../../models';
2
+ import { RobotHealth, RobotReadiness, RobotStatusContributors, ServiceConnection } from '../../models';
3
3
  import { TelemetryService } from '../TelemetryService';
4
4
  /** The new agent topic carrying health + readiness (replaces the heartbeat). */
5
5
  export declare const ROBOT_STATUS_SOURCE = "/diagnostics/robot/status";
6
6
  /** The legacy heartbeat topic — liveness only, no status payload. */
7
7
  export declare const LEGACY_HEARTBEAT_SOURCE = "/rocos/agent/telemetry/heartbeat";
8
+ /** The per-contributor breakdown behind the reported status (health + readiness). */
9
+ export declare const ROBOT_STATUS_CONTRIBUTORS_SOURCE = "/diagnostics/robot/status/contributors";
8
10
  export interface ReportedRobotStatus {
9
11
  health: RobotHealth;
10
12
  readiness: RobotReadiness;
@@ -28,6 +30,17 @@ export declare const parseRobotReadiness: (value: unknown) => RobotReadiness;
28
30
  * new topic.
29
31
  */
30
32
  export declare const getReportedStatusChanges: (telemetry: TelemetryService, projectId: string, callsign: string) => Observable<ReportedRobotStatus>;
33
+ /**
34
+ * Emits the per-contributor breakdown behind the robot's reported health and
35
+ * readiness, from the agent's `robot/status/contributors` topic. Each
36
+ * contributor's integer wire value is mapped onto {@link RobotHealth} /
37
+ * {@link RobotReadiness}; a missing `health`/`readiness` array becomes `[]`.
38
+ *
39
+ * Starts with an empty breakdown so consumers combining this with other streams
40
+ * emit immediately, and — like {@link getReportedStatusChanges} — a telemetry
41
+ * error falls back to an empty breakdown rather than terminating the stream.
42
+ */
43
+ export declare const getStatusContributorsChanges: (telemetry: TelemetryService, projectId: string, callsign: string) => Observable<RobotStatusContributors>;
31
44
  /**
32
45
  * Emits the `telemetry-liveness` connectivity signal: whether a heartbeat (from
33
46
  * either the new `robot/status` topic or the legacy heartbeat) has been received
@@ -1,9 +1,11 @@
1
1
  import { catchError, combineLatest, distinctUntilChanged, filter, interval, map, merge, of, scan, startWith, } from 'rxjs';
2
- import { RobotHealth, RobotReadiness, ServiceConnection } from '../../models';
2
+ import { RobotHealth, RobotReadiness, ServiceConnection, } from '../../models';
3
3
  /** The new agent topic carrying health + readiness (replaces the heartbeat). */
4
4
  export const ROBOT_STATUS_SOURCE = '/diagnostics/robot/status';
5
5
  /** The legacy heartbeat topic — liveness only, no status payload. */
6
6
  export const LEGACY_HEARTBEAT_SOURCE = '/rocos/agent/telemetry/heartbeat';
7
+ /** The per-contributor breakdown behind the reported status (health + readiness). */
8
+ export const ROBOT_STATUS_CONTRIBUTORS_SOURCE = `${ROBOT_STATUS_SOURCE}/contributors`;
7
9
  const DEFAULT_HEARTBEAT_TIMEOUT_MS = 5000;
8
10
  const DEFAULT_INTERVAL_MS = 2000;
9
11
  /**
@@ -78,6 +80,38 @@ export const getReportedStatusChanges = (telemetry, projectId, callsign) => {
78
80
  // UNKNOWN/UNKNOWN (matching getTelemetryLiveness) so subscribers get an honest value.
79
81
  catchError(() => of({ health: RobotHealth.UNKNOWN, readiness: RobotReadiness.UNKNOWN })));
80
82
  };
83
+ const toContributor = (raw, parse) => ({
84
+ id: raw.id ?? '',
85
+ component: raw.component ?? '',
86
+ reason: raw.reason ?? '',
87
+ status: parse(raw.value),
88
+ });
89
+ const EMPTY_CONTRIBUTORS = { health: [], readiness: [] };
90
+ /**
91
+ * Emits the per-contributor breakdown behind the robot's reported health and
92
+ * readiness, from the agent's `robot/status/contributors` topic. Each
93
+ * contributor's integer wire value is mapped onto {@link RobotHealth} /
94
+ * {@link RobotReadiness}; a missing `health`/`readiness` array becomes `[]`.
95
+ *
96
+ * Starts with an empty breakdown so consumers combining this with other streams
97
+ * emit immediately, and — like {@link getReportedStatusChanges} — a telemetry
98
+ * error falls back to an empty breakdown rather than terminating the stream.
99
+ */
100
+ export const getStatusContributorsChanges = (telemetry, projectId, callsign) => {
101
+ return telemetry
102
+ .subscribe({
103
+ projectId,
104
+ callsigns: [callsign],
105
+ sources: [ROBOT_STATUS_CONTRIBUTORS_SOURCE],
106
+ })
107
+ .pipe(map((message) => {
108
+ const payload = message.payload ?? {};
109
+ return {
110
+ health: (payload.health ?? []).map((c) => toContributor(c, parseRobotHealth)),
111
+ readiness: (payload.readiness ?? []).map((c) => toContributor(c, parseRobotReadiness)),
112
+ };
113
+ }), startWith(EMPTY_CONTRIBUTORS), distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), catchError(() => of(EMPTY_CONTRIBUTORS)));
114
+ };
81
115
  /**
82
116
  * Emits the `telemetry-liveness` connectivity signal: whether a heartbeat (from
83
117
  * either the new `robot/status` topic or the legacy heartbeat) has been received
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dronedeploy/rocos-js-sdk",
3
- "version": "4.4.5",
3
+ "version": "4.4.6",
4
4
  "description": "Javascript SDK for rocos",
5
5
  "main": "cjs/index.js",
6
6
  "module": "esm/index.js",
@@ -42,5 +42,10 @@
42
42
  }
43
43
  },
44
44
  "repository": {},
45
- "private": false
45
+ "private": false,
46
+ "overrides": {
47
+ "openapi-typescript": {
48
+ "typescript": "$typescript"
49
+ }
50
+ }
46
51
  }