@dronedeploy/rocos-js-sdk 4.1.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.
- package/cjs/IRocosSDK.d.ts +2 -1
- package/cjs/RocosSDK.d.ts +5 -1
- package/cjs/RocosSDK.js +9 -0
- package/cjs/constants/api.d.ts +8 -2
- package/cjs/constants/api.js +13 -5
- package/cjs/models/RocosError.d.ts +1 -0
- package/cjs/models/RocosError.js +1 -0
- package/cjs/models/ServiceEnum.d.ts +1 -0
- package/cjs/models/ServiceEnum.js +1 -0
- package/cjs/models/index.d.ts +1 -0
- package/cjs/models/index.js +1 -0
- package/cjs/models/maps/MapPath.d.ts +143 -0
- package/cjs/models/maps/MapPath.js +2 -0
- package/cjs/models/robot-status/RobotStatusEnums.d.ts +1 -1
- package/cjs/models/robot-status/RobotStatusEnums.js +1 -1
- package/cjs/services/MapService.d.ts +101 -0
- package/cjs/services/MapService.js +140 -0
- package/cjs/services/RobotStatusService.d.ts +37 -0
- package/cjs/services/RobotStatusService.js +94 -0
- package/cjs/services/index.d.ts +1 -0
- package/cjs/services/index.js +1 -0
- package/cjs/services/robot-status/connectivity.d.ts +76 -0
- package/cjs/services/robot-status/connectivity.js +140 -0
- package/cjs/services/robot-status/reportedStatus.d.ts +38 -0
- package/cjs/services/robot-status/reportedStatus.js +109 -0
- package/esm/IRocosSDK.d.ts +2 -1
- package/esm/RocosSDK.d.ts +5 -1
- package/esm/RocosSDK.js +10 -1
- package/esm/constants/api.d.ts +8 -2
- package/esm/constants/api.js +10 -2
- package/esm/models/RocosError.d.ts +1 -0
- package/esm/models/RocosError.js +1 -0
- package/esm/models/ServiceEnum.d.ts +1 -0
- package/esm/models/ServiceEnum.js +1 -0
- package/esm/models/index.d.ts +1 -0
- package/esm/models/index.js +1 -0
- package/esm/models/maps/MapPath.d.ts +143 -0
- package/esm/models/maps/MapPath.js +1 -0
- package/esm/models/robot-status/RobotStatusEnums.d.ts +1 -1
- package/esm/models/robot-status/RobotStatusEnums.js +1 -1
- package/esm/services/MapService.d.ts +101 -0
- package/esm/services/MapService.js +141 -1
- package/esm/services/RobotStatusService.d.ts +37 -0
- package/esm/services/RobotStatusService.js +90 -0
- package/esm/services/index.d.ts +1 -0
- package/esm/services/index.js +1 -0
- package/esm/services/robot-status/connectivity.d.ts +76 -0
- package/esm/services/robot-status/connectivity.js +131 -0
- package/esm/services/robot-status/reportedStatus.d.ts +38 -0
- package/esm/services/robot-status/reportedStatus.js +102 -0
- package/package.json +1 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { IConnectedCallsign, RobotConnectivity, RobotConnectivityStatus, ServiceConnection } from '../../models';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
/**
|
|
4
|
+
* Service id → public path prefix, for services that expose the standard
|
|
5
|
+
* `connections` endpoint. New services adopting the convention are added here.
|
|
6
|
+
* The `telemetry` (Teletubby) endpoint joins this once built — see EAI-718/719.
|
|
7
|
+
*/
|
|
8
|
+
export declare const CONNECTION_SERVICE_PREFIXES: Record<string, string>;
|
|
9
|
+
/**
|
|
10
|
+
* The telemetry-liveness signal is derived from heartbeat freshness rather than
|
|
11
|
+
* an HTTP endpoint, so it is handled specially (its observable is supplied by
|
|
12
|
+
* the caller). See `getTelemetryLiveness`.
|
|
13
|
+
*/
|
|
14
|
+
export declare const TELEMETRY_LIVENESS_SERVICE = "telemetry-liveness";
|
|
15
|
+
/**
|
|
16
|
+
* The services evaluated by default. The `telemetry` connections endpoint joins
|
|
17
|
+
* this list once Teletubby exposes it (follow-up).
|
|
18
|
+
*/
|
|
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;
|
|
28
|
+
/** Authenticated GET used to poll a service's `connections` endpoint. */
|
|
29
|
+
export type ConnectionsHttpGet = (url: string) => Promise<IConnectedCallsign>;
|
|
30
|
+
/**
|
|
31
|
+
* Builds the `connections` endpoint URL for a service — per-callsign when a
|
|
32
|
+
* callsign is given, otherwise the bulk per-project variant.
|
|
33
|
+
*/
|
|
34
|
+
export declare const buildConnectionsUrl: (serviceId: string, baseUrl: string, projectId: string, callsign?: string, insecure?: boolean) => string;
|
|
35
|
+
/** Maps a `connections` endpoint response onto a {@link ServiceConnection}. */
|
|
36
|
+
export declare const mapConnectionStatus: (connection?: IConnectedCallsign | null) => ServiceConnection;
|
|
37
|
+
/**
|
|
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.
|
|
41
|
+
* - `OFFLINE` — nothing connected.
|
|
42
|
+
*
|
|
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.
|
|
45
|
+
*
|
|
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.
|
|
51
|
+
*/
|
|
52
|
+
export declare const aggregateConnectivity: (services: Record<string, ServiceConnection>) => RobotConnectivity;
|
|
53
|
+
/**
|
|
54
|
+
* Polls a single service's `connections` endpoint on an interval, emitting its
|
|
55
|
+
* {@link ServiceConnection} (change-only). A failed/timed-out poll yields
|
|
56
|
+
* `UNKNOWN` (held transitional) rather than `DISCONNECTED`, to avoid a spurious
|
|
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.
|
|
61
|
+
*/
|
|
62
|
+
export declare const getServiceConnectionChanges: (httpGet: ConnectionsHttpGet, url: string, pollMs?: number) => Observable<ServiceConnection>;
|
|
63
|
+
export interface ConnectivityDeps {
|
|
64
|
+
httpGet: ConnectionsHttpGet;
|
|
65
|
+
baseUrl: string;
|
|
66
|
+
insecure?: boolean;
|
|
67
|
+
/** The telemetry-liveness signal (from `getTelemetryLiveness`), folded in as one service. */
|
|
68
|
+
telemetryLiveness$: Observable<ServiceConnection>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 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
|
+
*/
|
|
76
|
+
export declare const getConnectivityChanges: (deps: ConnectivityDeps, projectId: string, callsign: string, expectedServices?: string[], pollMs?: number) => Observable<RobotConnectivityStatus>;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { API_SERVICE_CONNECTIONS_URL, API_SERVICE_CONNECTION_URL } from '../../constants/api';
|
|
2
|
+
import { ConnectionStatus, RobotConnectivity, ServiceConnection, } from '../../models';
|
|
3
|
+
import { catchError, combineLatest, distinctUntilChanged, from, map, of, startWith, switchMap, timer, } from 'rxjs';
|
|
4
|
+
import { formatServiceUrl } from '../../helpers/formatServiceUrl';
|
|
5
|
+
/**
|
|
6
|
+
* Service id → public path prefix, for services that expose the standard
|
|
7
|
+
* `connections` endpoint. New services adopting the convention are added here.
|
|
8
|
+
* The `telemetry` (Teletubby) endpoint joins this once built — see EAI-718/719.
|
|
9
|
+
*/
|
|
10
|
+
export const CONNECTION_SERVICE_PREFIXES = {
|
|
11
|
+
'robot-configs': 'robot-configs',
|
|
12
|
+
'robot-services': 'robot-services',
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The telemetry-liveness signal is derived from heartbeat freshness rather than
|
|
16
|
+
* an HTTP endpoint, so it is handled specially (its observable is supplied by
|
|
17
|
+
* the caller). See `getTelemetryLiveness`.
|
|
18
|
+
*/
|
|
19
|
+
export const TELEMETRY_LIVENESS_SERVICE = 'telemetry-liveness';
|
|
20
|
+
/**
|
|
21
|
+
* The services evaluated by default. The `telemetry` connections endpoint joins
|
|
22
|
+
* this list once Teletubby exposes it (follow-up).
|
|
23
|
+
*/
|
|
24
|
+
export const DEFAULT_EXPECTED_SERVICES = [TELEMETRY_LIVENESS_SERVICE, 'robot-configs', 'robot-services'];
|
|
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);
|
|
34
|
+
/**
|
|
35
|
+
* Builds the `connections` endpoint URL for a service — per-callsign when a
|
|
36
|
+
* callsign is given, otherwise the bulk per-project variant.
|
|
37
|
+
*/
|
|
38
|
+
export const buildConnectionsUrl = (serviceId, baseUrl, projectId, callsign, insecure) => {
|
|
39
|
+
const template = callsign ? API_SERVICE_CONNECTION_URL : API_SERVICE_CONNECTIONS_URL;
|
|
40
|
+
const params = { url: baseUrl, service: CONNECTION_SERVICE_PREFIXES[serviceId], projectId };
|
|
41
|
+
if (callsign)
|
|
42
|
+
params.callsign = callsign;
|
|
43
|
+
return formatServiceUrl(template, params, insecure);
|
|
44
|
+
};
|
|
45
|
+
/** Maps a `connections` endpoint response onto a {@link ServiceConnection}. */
|
|
46
|
+
export const mapConnectionStatus = (connection) => {
|
|
47
|
+
if (connection?.status === ConnectionStatus.CONNECTED)
|
|
48
|
+
return ServiceConnection.CONNECTED;
|
|
49
|
+
if (connection?.status === ConnectionStatus.DISCONNECTED)
|
|
50
|
+
return ServiceConnection.DISCONNECTED;
|
|
51
|
+
return ServiceConnection.UNKNOWN;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
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.
|
|
57
|
+
* - `OFFLINE` — nothing connected.
|
|
58
|
+
*
|
|
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.
|
|
61
|
+
*
|
|
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.
|
|
67
|
+
*/
|
|
68
|
+
export const aggregateConnectivity = (services) => {
|
|
69
|
+
const values = Object.values(services);
|
|
70
|
+
const hasConnected = values.includes(ServiceConnection.CONNECTED);
|
|
71
|
+
const hasDisconnected = values.includes(ServiceConnection.DISCONNECTED);
|
|
72
|
+
if (hasConnected && hasDisconnected)
|
|
73
|
+
return RobotConnectivity.DEGRADED;
|
|
74
|
+
if (hasConnected)
|
|
75
|
+
return RobotConnectivity.ONLINE;
|
|
76
|
+
return RobotConnectivity.OFFLINE;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Polls a single service's `connections` endpoint on an interval, emitting its
|
|
80
|
+
* {@link ServiceConnection} (change-only). A failed/timed-out poll yields
|
|
81
|
+
* `UNKNOWN` (held transitional) rather than `DISCONNECTED`, to avoid a spurious
|
|
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.
|
|
86
|
+
*/
|
|
87
|
+
export const getServiceConnectionChanges = (httpGet, url, pollMs = DEFAULT_CONNECTIVITY_POLL_MS) => {
|
|
88
|
+
return timer(0, clampPollInterval(pollMs)).pipe(switchMap(() => {
|
|
89
|
+
return from(httpGet(url)).pipe(map(mapConnectionStatus), catchError(() => of(ServiceConnection.UNKNOWN)));
|
|
90
|
+
}), startWith(ServiceConnection.UNKNOWN), distinctUntilChanged());
|
|
91
|
+
};
|
|
92
|
+
const connectivityEquals = (a, b) => {
|
|
93
|
+
if (a.overall !== b.overall)
|
|
94
|
+
return false;
|
|
95
|
+
const keys = Object.keys(a.services);
|
|
96
|
+
if (keys.length !== Object.keys(b.services).length)
|
|
97
|
+
return false;
|
|
98
|
+
return keys.every((key) => a.services[key] === b.services[key]);
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* 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
|
+
*/
|
|
106
|
+
export const getConnectivityChanges = (deps, projectId, callsign, expectedServices = DEFAULT_EXPECTED_SERVICES, pollMs = DEFAULT_CONNECTIVITY_POLL_MS) => {
|
|
107
|
+
// combineLatest([]) completes without emitting; emit an explicit empty/offline status instead.
|
|
108
|
+
if (expectedServices.length === 0) {
|
|
109
|
+
return of({ overall: RobotConnectivity.OFFLINE, services: {} });
|
|
110
|
+
}
|
|
111
|
+
const perService = expectedServices.map((serviceId) => {
|
|
112
|
+
let source$;
|
|
113
|
+
if (serviceId === TELEMETRY_LIVENESS_SERVICE) {
|
|
114
|
+
source$ = deps.telemetryLiveness$;
|
|
115
|
+
}
|
|
116
|
+
else if (serviceId in CONNECTION_SERVICE_PREFIXES) {
|
|
117
|
+
const url = buildConnectionsUrl(serviceId, deps.baseUrl, projectId, callsign, deps.insecure);
|
|
118
|
+
source$ = getServiceConnectionChanges(deps.httpGet, url, pollMs);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
source$ = of(ServiceConnection.UNKNOWN);
|
|
122
|
+
}
|
|
123
|
+
return source$.pipe(startWith(ServiceConnection.UNKNOWN), map((connection) => ({ serviceId, connection })));
|
|
124
|
+
});
|
|
125
|
+
return combineLatest(perService).pipe(map((entries) => {
|
|
126
|
+
const services = {};
|
|
127
|
+
for (const { serviceId, connection } of entries)
|
|
128
|
+
services[serviceId] = connection;
|
|
129
|
+
return { overall: aggregateConnectivity(services), services };
|
|
130
|
+
}), distinctUntilChanged(connectivityEquals));
|
|
131
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
import { RobotHealth, RobotReadiness, ServiceConnection } from '../../models';
|
|
3
|
+
import { TelemetryService } from '../TelemetryService';
|
|
4
|
+
/** The new agent topic carrying health + readiness (replaces the heartbeat). */
|
|
5
|
+
export declare const ROBOT_STATUS_SOURCE = "/diagnostics/robot/status";
|
|
6
|
+
/** The legacy heartbeat topic — liveness only, no status payload. */
|
|
7
|
+
export declare const LEGACY_HEARTBEAT_SOURCE = "/rocos/agent/telemetry/heartbeat";
|
|
8
|
+
export interface ReportedRobotStatus {
|
|
9
|
+
health: RobotHealth;
|
|
10
|
+
readiness: RobotReadiness;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Maps a `robot/status` wire value (integer, or its string equivalent) to a
|
|
14
|
+
* {@link RobotHealth}. Anything unrecognised — including `0` / `'unknown'` —
|
|
15
|
+
* resolves to {@link RobotHealth.UNKNOWN}.
|
|
16
|
+
*/
|
|
17
|
+
export declare const parseRobotHealth: (value: unknown) => RobotHealth;
|
|
18
|
+
/** As {@link parseRobotHealth}, for {@link RobotReadiness}. */
|
|
19
|
+
export declare const parseRobotReadiness: (value: unknown) => RobotReadiness;
|
|
20
|
+
/**
|
|
21
|
+
* Emits the robot's reported health/readiness, subscribing to the new
|
|
22
|
+
* `robot/status` topic and the legacy heartbeat concurrently.
|
|
23
|
+
*
|
|
24
|
+
* The new topic takes precedence: once any `robot/status` message has been seen,
|
|
25
|
+
* legacy heartbeats no longer affect the reported value. While only the legacy
|
|
26
|
+
* heartbeat is present, a live heartbeat is reported as `NORMAL`/`IDLE` — which
|
|
27
|
+
* preserves today's "online ⇒ green" behaviour for agents not yet emitting the
|
|
28
|
+
* new topic.
|
|
29
|
+
*/
|
|
30
|
+
export declare const getReportedStatusChanges: (telemetry: TelemetryService, projectId: string, callsign: string) => Observable<ReportedRobotStatus>;
|
|
31
|
+
/**
|
|
32
|
+
* Emits the `telemetry-liveness` connectivity signal: whether a heartbeat (from
|
|
33
|
+
* either the new `robot/status` topic or the legacy heartbeat) has been received
|
|
34
|
+
* within `heartbeatTimeoutMs`. Mirrors {@link TelemetryService.monitorTelemetryWithTimeout}
|
|
35
|
+
* but spans both topics and maps onto {@link ServiceConnection} so it can be
|
|
36
|
+
* folded into connectivity aggregation as just another service entry.
|
|
37
|
+
*/
|
|
38
|
+
export declare const getTelemetryLiveness: (telemetry: TelemetryService, projectId: string, callsign: string, heartbeatTimeoutMs?: number, intervalMs?: number) => Observable<ServiceConnection>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { catchError, combineLatest, distinctUntilChanged, filter, interval, map, merge, of, scan, startWith, } from 'rxjs';
|
|
2
|
+
import { RobotHealth, RobotReadiness, ServiceConnection } from '../../models';
|
|
3
|
+
/** The new agent topic carrying health + readiness (replaces the heartbeat). */
|
|
4
|
+
export const ROBOT_STATUS_SOURCE = '/diagnostics/robot/status';
|
|
5
|
+
/** The legacy heartbeat topic — liveness only, no status payload. */
|
|
6
|
+
export const LEGACY_HEARTBEAT_SOURCE = '/rocos/agent/telemetry/heartbeat';
|
|
7
|
+
const DEFAULT_HEARTBEAT_TIMEOUT_MS = 5000;
|
|
8
|
+
const DEFAULT_INTERVAL_MS = 2000;
|
|
9
|
+
/**
|
|
10
|
+
* Maps a `robot/status` wire value (integer, or its string equivalent) to a
|
|
11
|
+
* {@link RobotHealth}. Anything unrecognised — including `0` / `'unknown'` —
|
|
12
|
+
* resolves to {@link RobotHealth.UNKNOWN}.
|
|
13
|
+
*/
|
|
14
|
+
export const parseRobotHealth = (value) => {
|
|
15
|
+
switch (value) {
|
|
16
|
+
case 1:
|
|
17
|
+
case '1':
|
|
18
|
+
case 'normal':
|
|
19
|
+
return RobotHealth.NORMAL;
|
|
20
|
+
case 2:
|
|
21
|
+
case '2':
|
|
22
|
+
case 'abnormal':
|
|
23
|
+
return RobotHealth.ABNORMAL;
|
|
24
|
+
case 3:
|
|
25
|
+
case '3':
|
|
26
|
+
case 'critical':
|
|
27
|
+
return RobotHealth.CRITICAL;
|
|
28
|
+
default:
|
|
29
|
+
return RobotHealth.UNKNOWN;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/** As {@link parseRobotHealth}, for {@link RobotReadiness}. */
|
|
33
|
+
export const parseRobotReadiness = (value) => {
|
|
34
|
+
switch (value) {
|
|
35
|
+
case 1:
|
|
36
|
+
case '1':
|
|
37
|
+
case 'idle':
|
|
38
|
+
return RobotReadiness.IDLE;
|
|
39
|
+
case 2:
|
|
40
|
+
case '2':
|
|
41
|
+
case 'busy':
|
|
42
|
+
return RobotReadiness.BUSY;
|
|
43
|
+
default:
|
|
44
|
+
return RobotReadiness.UNKNOWN;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Emits the robot's reported health/readiness, subscribing to the new
|
|
49
|
+
* `robot/status` topic and the legacy heartbeat concurrently.
|
|
50
|
+
*
|
|
51
|
+
* The new topic takes precedence: once any `robot/status` message has been seen,
|
|
52
|
+
* legacy heartbeats no longer affect the reported value. While only the legacy
|
|
53
|
+
* heartbeat is present, a live heartbeat is reported as `NORMAL`/`IDLE` — which
|
|
54
|
+
* preserves today's "online ⇒ green" behaviour for agents not yet emitting the
|
|
55
|
+
* new topic.
|
|
56
|
+
*/
|
|
57
|
+
export const getReportedStatusChanges = (telemetry, projectId, callsign) => {
|
|
58
|
+
const new$ = telemetry
|
|
59
|
+
.subscribe({ projectId, callsigns: [callsign], sources: [ROBOT_STATUS_SOURCE] })
|
|
60
|
+
.pipe(map((message) => ({
|
|
61
|
+
from: 'new',
|
|
62
|
+
status: {
|
|
63
|
+
health: parseRobotHealth(message.payload?.health),
|
|
64
|
+
readiness: parseRobotReadiness(message.payload?.readiness),
|
|
65
|
+
},
|
|
66
|
+
})));
|
|
67
|
+
const legacy$ = telemetry.subscribe({ projectId, callsigns: [callsign], sources: [LEGACY_HEARTBEAT_SOURCE] }).pipe(map(() => ({
|
|
68
|
+
from: 'legacy',
|
|
69
|
+
status: { health: RobotHealth.NORMAL, readiness: RobotReadiness.IDLE },
|
|
70
|
+
})));
|
|
71
|
+
return merge(new$, legacy$).pipe(scan((acc, event) => {
|
|
72
|
+
if (event.from === 'new')
|
|
73
|
+
return { seenNew: true, status: event.status };
|
|
74
|
+
// Legacy heartbeat: only honoured until the new topic has been seen.
|
|
75
|
+
return acc.seenNew ? acc : { seenNew: false, status: event.status };
|
|
76
|
+
}, { seenNew: false, status: undefined }), map((acc) => acc.status), filter((status) => status !== undefined), distinctUntilChanged((a, b) => a.health === b.health && a.readiness === b.readiness),
|
|
77
|
+
// A telemetry error would otherwise terminate this stream permanently; fall back to
|
|
78
|
+
// UNKNOWN/UNKNOWN (matching getTelemetryLiveness) so subscribers get an honest value.
|
|
79
|
+
catchError(() => of({ health: RobotHealth.UNKNOWN, readiness: RobotReadiness.UNKNOWN })));
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Emits the `telemetry-liveness` connectivity signal: whether a heartbeat (from
|
|
83
|
+
* either the new `robot/status` topic or the legacy heartbeat) has been received
|
|
84
|
+
* within `heartbeatTimeoutMs`. Mirrors {@link TelemetryService.monitorTelemetryWithTimeout}
|
|
85
|
+
* but spans both topics and maps onto {@link ServiceConnection} so it can be
|
|
86
|
+
* folded into connectivity aggregation as just another service entry.
|
|
87
|
+
*/
|
|
88
|
+
export const getTelemetryLiveness = (telemetry, projectId, callsign, heartbeatTimeoutMs = DEFAULT_HEARTBEAT_TIMEOUT_MS, intervalMs = DEFAULT_INTERVAL_MS) => {
|
|
89
|
+
const startedAt = Date.now();
|
|
90
|
+
const lastMessageAt$ = telemetry
|
|
91
|
+
.subscribe({ projectId, callsigns: [callsign], sources: [ROBOT_STATUS_SOURCE, LEGACY_HEARTBEAT_SOURCE] })
|
|
92
|
+
.pipe(map(() => Date.now()));
|
|
93
|
+
return combineLatest([lastMessageAt$.pipe(startWith(startedAt)), interval(intervalMs)]).pipe(map(([lastMessageAt]) => {
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
// No message yet, but still within the initial grace period.
|
|
96
|
+
if (lastMessageAt === startedAt && now - startedAt <= heartbeatTimeoutMs)
|
|
97
|
+
return ServiceConnection.UNKNOWN;
|
|
98
|
+
if (now - lastMessageAt > heartbeatTimeoutMs)
|
|
99
|
+
return ServiceConnection.DISCONNECTED;
|
|
100
|
+
return ServiceConnection.CONNECTED;
|
|
101
|
+
}), startWith(ServiceConnection.UNKNOWN), distinctUntilChanged(), catchError(() => of(ServiceConnection.UNKNOWN)));
|
|
102
|
+
};
|