@dronedeploy/rocos-js-sdk 4.1.0 → 4.2.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.
@@ -0,0 +1,128 @@
1
+ "use strict";
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;
4
+ const api_1 = require("../../constants/api");
5
+ const models_1 = require("../../models");
6
+ const rxjs_1 = require("rxjs");
7
+ const formatServiceUrl_1 = require("../../helpers/formatServiceUrl");
8
+ /**
9
+ * Service id → public path prefix, for services that expose the standard
10
+ * `connections` endpoint. New services adopting the convention are added here.
11
+ * The `telemetry` (Teletubby) endpoint joins this once built — see EAI-718/719.
12
+ */
13
+ exports.CONNECTION_SERVICE_PREFIXES = {
14
+ 'robot-configs': 'robot-configs',
15
+ 'robot-services': 'robot-services',
16
+ };
17
+ /**
18
+ * The telemetry-liveness signal is derived from heartbeat freshness rather than
19
+ * an HTTP endpoint, so it is handled specially (its observable is supplied by
20
+ * the caller). See `getTelemetryLiveness`.
21
+ */
22
+ exports.TELEMETRY_LIVENESS_SERVICE = 'telemetry-liveness';
23
+ /**
24
+ * The services evaluated by default. The `telemetry` connections endpoint joins
25
+ * this list once Teletubby exposes it (follow-up).
26
+ */
27
+ exports.DEFAULT_EXPECTED_SERVICES = [exports.TELEMETRY_LIVENESS_SERVICE, 'robot-configs', 'robot-services'];
28
+ const DEFAULT_CONNECTIVITY_POLL_MS = 5000;
29
+ /**
30
+ * Builds the `connections` endpoint URL for a service — per-callsign when a
31
+ * callsign is given, otherwise the bulk per-project variant.
32
+ */
33
+ const buildConnectionsUrl = (serviceId, baseUrl, projectId, callsign, insecure) => {
34
+ const template = callsign ? api_1.API_SERVICE_CONNECTION_URL : api_1.API_SERVICE_CONNECTIONS_URL;
35
+ const params = { url: baseUrl, service: exports.CONNECTION_SERVICE_PREFIXES[serviceId], projectId };
36
+ if (callsign)
37
+ params.callsign = callsign;
38
+ return (0, formatServiceUrl_1.formatServiceUrl)(template, params, insecure);
39
+ };
40
+ exports.buildConnectionsUrl = buildConnectionsUrl;
41
+ /** Maps a `connections` endpoint response onto a {@link ServiceConnection}. */
42
+ const mapConnectionStatus = (connection) => {
43
+ if (connection?.status === models_1.ConnectionStatus.CONNECTED)
44
+ return models_1.ServiceConnection.CONNECTED;
45
+ if (connection?.status === models_1.ConnectionStatus.DISCONNECTED)
46
+ return models_1.ServiceConnection.DISCONNECTED;
47
+ return models_1.ServiceConnection.UNKNOWN;
48
+ };
49
+ exports.mapConnectionStatus = mapConnectionStatus;
50
+ /**
51
+ * Aggregates per-service connection states into an overall verdict:
52
+ * - `ONLINE` — at least one service connected and none disconnected.
53
+ * - `DEGRADED` — a genuine mix: some connected, some disconnected.
54
+ * - `OFFLINE` — nothing connected.
55
+ *
56
+ * `UNKNOWN` services (e.g. not yet polled) never force `DEGRADED`, so startup
57
+ * settles on `OFFLINE` (gray) rather than a spurious flashing-red degraded state.
58
+ *
59
+ * Note: this optimism also means a service held `UNKNOWN` indefinitely (e.g. an
60
+ * endpoint whose polls keep failing) does not by itself pull the aggregate below
61
+ * `ONLINE` — a deliberate v1 tradeoff to keep transient errors from flapping the
62
+ * indicator. Surfacing a long-lived failure distinctly (likely via health rather
63
+ * than connectivity) is left as a future refinement.
64
+ */
65
+ const aggregateConnectivity = (services) => {
66
+ const values = Object.values(services);
67
+ const hasConnected = values.includes(models_1.ServiceConnection.CONNECTED);
68
+ const hasDisconnected = values.includes(models_1.ServiceConnection.DISCONNECTED);
69
+ if (hasConnected && hasDisconnected)
70
+ return models_1.RobotConnectivity.DEGRADED;
71
+ if (hasConnected)
72
+ return models_1.RobotConnectivity.ONLINE;
73
+ return models_1.RobotConnectivity.OFFLINE;
74
+ };
75
+ exports.aggregateConnectivity = aggregateConnectivity;
76
+ /**
77
+ * Polls a single service's `connections` endpoint on an interval, emitting its
78
+ * {@link ServiceConnection} (change-only). A failed/timed-out poll yields
79
+ * `UNKNOWN` (held transitional) rather than `DISCONNECTED`, to avoid a spurious
80
+ * degraded flash on a transient error.
81
+ */
82
+ const getServiceConnectionChanges = (httpGet, url, pollMs = DEFAULT_CONNECTIVITY_POLL_MS) => {
83
+ return (0, rxjs_1.timer)(0, pollMs).pipe((0, rxjs_1.switchMap)(() => {
84
+ 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
+ }), (0, rxjs_1.startWith)(models_1.ServiceConnection.UNKNOWN), (0, rxjs_1.distinctUntilChanged)());
86
+ };
87
+ exports.getServiceConnectionChanges = getServiceConnectionChanges;
88
+ const connectivityEquals = (a, b) => {
89
+ if (a.overall !== b.overall)
90
+ return false;
91
+ const keys = Object.keys(a.services);
92
+ if (keys.length !== Object.keys(b.services).length)
93
+ return false;
94
+ return keys.every((key) => a.services[key] === b.services[key]);
95
+ };
96
+ /**
97
+ * Emits the robot's {@link RobotConnectivityStatus} — the per-service breakdown
98
+ * plus the aggregate verdict — for the configured `expectedServices`. HTTP
99
+ * services are polled; `telemetry-liveness` uses the supplied observable; any
100
+ * unrecognised service id resolves to `UNKNOWN`.
101
+ */
102
+ const getConnectivityChanges = (deps, projectId, callsign, expectedServices = exports.DEFAULT_EXPECTED_SERVICES, pollMs = DEFAULT_CONNECTIVITY_POLL_MS) => {
103
+ // combineLatest([]) completes without emitting; emit an explicit empty/offline status instead.
104
+ if (expectedServices.length === 0) {
105
+ return (0, rxjs_1.of)({ overall: models_1.RobotConnectivity.OFFLINE, services: {} });
106
+ }
107
+ const perService = expectedServices.map((serviceId) => {
108
+ let source$;
109
+ if (serviceId === exports.TELEMETRY_LIVENESS_SERVICE) {
110
+ source$ = deps.telemetryLiveness$;
111
+ }
112
+ else if (serviceId in exports.CONNECTION_SERVICE_PREFIXES) {
113
+ const url = (0, exports.buildConnectionsUrl)(serviceId, deps.baseUrl, projectId, callsign, deps.insecure);
114
+ source$ = (0, exports.getServiceConnectionChanges)(deps.httpGet, url, pollMs);
115
+ }
116
+ else {
117
+ source$ = (0, rxjs_1.of)(models_1.ServiceConnection.UNKNOWN);
118
+ }
119
+ return source$.pipe((0, rxjs_1.startWith)(models_1.ServiceConnection.UNKNOWN), (0, rxjs_1.map)((connection) => ({ serviceId, connection })));
120
+ });
121
+ return (0, rxjs_1.combineLatest)(perService).pipe((0, rxjs_1.map)((entries) => {
122
+ const services = {};
123
+ for (const { serviceId, connection } of entries)
124
+ services[serviceId] = connection;
125
+ return { overall: (0, exports.aggregateConnectivity)(services), services };
126
+ }), (0, rxjs_1.distinctUntilChanged)(connectivityEquals));
127
+ };
128
+ exports.getConnectivityChanges = getConnectivityChanges;
@@ -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,109 @@
1
+ "use strict";
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;
4
+ const rxjs_1 = require("rxjs");
5
+ const models_1 = require("../../models");
6
+ /** The new agent topic carrying health + readiness (replaces the heartbeat). */
7
+ exports.ROBOT_STATUS_SOURCE = '/diagnostics/robot/status';
8
+ /** The legacy heartbeat topic — liveness only, no status payload. */
9
+ exports.LEGACY_HEARTBEAT_SOURCE = '/rocos/agent/telemetry/heartbeat';
10
+ const DEFAULT_HEARTBEAT_TIMEOUT_MS = 5000;
11
+ const DEFAULT_INTERVAL_MS = 2000;
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
+ const parseRobotHealth = (value) => {
18
+ switch (value) {
19
+ case 1:
20
+ case '1':
21
+ case 'normal':
22
+ return models_1.RobotHealth.NORMAL;
23
+ case 2:
24
+ case '2':
25
+ case 'abnormal':
26
+ return models_1.RobotHealth.ABNORMAL;
27
+ case 3:
28
+ case '3':
29
+ case 'critical':
30
+ return models_1.RobotHealth.CRITICAL;
31
+ default:
32
+ return models_1.RobotHealth.UNKNOWN;
33
+ }
34
+ };
35
+ exports.parseRobotHealth = parseRobotHealth;
36
+ /** As {@link parseRobotHealth}, for {@link RobotReadiness}. */
37
+ const parseRobotReadiness = (value) => {
38
+ switch (value) {
39
+ case 1:
40
+ case '1':
41
+ case 'idle':
42
+ return models_1.RobotReadiness.IDLE;
43
+ case 2:
44
+ case '2':
45
+ case 'busy':
46
+ return models_1.RobotReadiness.BUSY;
47
+ default:
48
+ return models_1.RobotReadiness.UNKNOWN;
49
+ }
50
+ };
51
+ exports.parseRobotReadiness = parseRobotReadiness;
52
+ /**
53
+ * Emits the robot's reported health/readiness, subscribing to the new
54
+ * `robot/status` topic and the legacy heartbeat concurrently.
55
+ *
56
+ * The new topic takes precedence: once any `robot/status` message has been seen,
57
+ * legacy heartbeats no longer affect the reported value. While only the legacy
58
+ * heartbeat is present, a live heartbeat is reported as `NORMAL`/`IDLE` — which
59
+ * preserves today's "online ⇒ green" behaviour for agents not yet emitting the
60
+ * new topic.
61
+ */
62
+ const getReportedStatusChanges = (telemetry, projectId, callsign) => {
63
+ const new$ = telemetry
64
+ .subscribe({ projectId, callsigns: [callsign], sources: [exports.ROBOT_STATUS_SOURCE] })
65
+ .pipe((0, rxjs_1.map)((message) => ({
66
+ from: 'new',
67
+ status: {
68
+ health: (0, exports.parseRobotHealth)(message.payload?.health),
69
+ readiness: (0, exports.parseRobotReadiness)(message.payload?.readiness),
70
+ },
71
+ })));
72
+ const legacy$ = telemetry.subscribe({ projectId, callsigns: [callsign], sources: [exports.LEGACY_HEARTBEAT_SOURCE] }).pipe((0, rxjs_1.map)(() => ({
73
+ from: 'legacy',
74
+ status: { health: models_1.RobotHealth.NORMAL, readiness: models_1.RobotReadiness.IDLE },
75
+ })));
76
+ return (0, rxjs_1.merge)(new$, legacy$).pipe((0, rxjs_1.scan)((acc, event) => {
77
+ if (event.from === 'new')
78
+ return { seenNew: true, status: event.status };
79
+ // Legacy heartbeat: only honoured until the new topic has been seen.
80
+ return acc.seenNew ? acc : { seenNew: false, status: event.status };
81
+ }, { seenNew: false, status: undefined }), (0, rxjs_1.map)((acc) => acc.status), (0, rxjs_1.filter)((status) => status !== undefined), (0, rxjs_1.distinctUntilChanged)((a, b) => a.health === b.health && a.readiness === b.readiness),
82
+ // A telemetry error would otherwise terminate this stream permanently; fall back to
83
+ // UNKNOWN/UNKNOWN (matching getTelemetryLiveness) so subscribers get an honest value.
84
+ (0, rxjs_1.catchError)(() => (0, rxjs_1.of)({ health: models_1.RobotHealth.UNKNOWN, readiness: models_1.RobotReadiness.UNKNOWN })));
85
+ };
86
+ exports.getReportedStatusChanges = getReportedStatusChanges;
87
+ /**
88
+ * Emits the `telemetry-liveness` connectivity signal: whether a heartbeat (from
89
+ * either the new `robot/status` topic or the legacy heartbeat) has been received
90
+ * within `heartbeatTimeoutMs`. Mirrors {@link TelemetryService.monitorTelemetryWithTimeout}
91
+ * but spans both topics and maps onto {@link ServiceConnection} so it can be
92
+ * folded into connectivity aggregation as just another service entry.
93
+ */
94
+ const getTelemetryLiveness = (telemetry, projectId, callsign, heartbeatTimeoutMs = DEFAULT_HEARTBEAT_TIMEOUT_MS, intervalMs = DEFAULT_INTERVAL_MS) => {
95
+ const startedAt = Date.now();
96
+ const lastMessageAt$ = telemetry
97
+ .subscribe({ projectId, callsigns: [callsign], sources: [exports.ROBOT_STATUS_SOURCE, exports.LEGACY_HEARTBEAT_SOURCE] })
98
+ .pipe((0, rxjs_1.map)(() => Date.now()));
99
+ return (0, rxjs_1.combineLatest)([lastMessageAt$.pipe((0, rxjs_1.startWith)(startedAt)), (0, rxjs_1.interval)(intervalMs)]).pipe((0, rxjs_1.map)(([lastMessageAt]) => {
100
+ const now = Date.now();
101
+ // No message yet, but still within the initial grace period.
102
+ if (lastMessageAt === startedAt && now - startedAt <= heartbeatTimeoutMs)
103
+ return models_1.ServiceConnection.UNKNOWN;
104
+ if (now - lastMessageAt > heartbeatTimeoutMs)
105
+ return models_1.ServiceConnection.DISCONNECTED;
106
+ return models_1.ServiceConnection.CONNECTED;
107
+ }), (0, rxjs_1.startWith)(models_1.ServiceConnection.UNKNOWN), (0, rxjs_1.distinctUntilChanged)(), (0, rxjs_1.catchError)(() => (0, rxjs_1.of)(models_1.ServiceConnection.UNKNOWN)));
108
+ };
109
+ exports.getTelemetryLiveness = getTelemetryLiveness;
@@ -56,8 +56,10 @@ export declare const API_PROJECT_ROBOT_COMMAND2_URL = "https://{url}/projects/{p
56
56
  export declare const API_PROJECT_ROBOT_BUTTON_URL = "https://{url}/projects/{projectId}/robots/{callsign}/buttons";
57
57
  export declare const API_PROJECT_ROBOT_TRIGGER_URL = "https://{url}/projects/{projectId}/robots/{callsign}/triggers";
58
58
  export declare const API_PROJECT_ROBOT_GAMEPAD_URL = "https://{url}/projects/{projectId}/robots/{callsign}/gamepads";
59
- export declare const API_PROJECT_ROBOT_CONFIGS_CONNECTIONS_URL = "https://{url}/robot-configs/connections/projects/{projectId}";
60
- export declare const API_PROJECT_ROBOT_CONFIGS_CONNECTION_URL = "https://{url}/robot-configs/connections/projects/{projectId}/callsigns/{callsign}";
59
+ export declare const API_SERVICE_CONNECTIONS_URL = "https://{url}/{service}/connections/projects/{projectId}";
60
+ export declare const API_SERVICE_CONNECTION_URL = "https://{url}/{service}/connections/projects/{projectId}/callsigns/{callsign}";
61
+ export declare const API_PROJECT_ROBOT_CONFIGS_CONNECTIONS_URL: string;
62
+ export declare const API_PROJECT_ROBOT_CONFIGS_CONNECTION_URL: string;
61
63
  export declare const API_PROJECT_OPERATION_URL = "https://{url}/projects/{projectId}/operations";
62
64
  export declare const API_PROJECT_OPERATION_ID_URL = "https://{url}/projects/{projectId}/operations/{operationId}";
63
65
  export declare const API_PROJECT_DASHBOARD_URL = "https://{url}/projects/{projectId}/dashboards";
@@ -134,6 +136,10 @@ export declare const API_GRAPHS_MAP_NODE_URL = "https://{url}/graphs/projects/{p
134
136
  export declare const API_GRAPHS_MAP_EDGES_ADD_URL = "https://{url}/graphs/projects/{projectId}/maps/{mapId}/edges/add";
135
137
  export declare const API_GRAPHS_MAP_EDGES_ADD_TRANSFORM_URL = "https://{url}/graphs/projects/{projectId}/maps/{mapId}/edges/add/transform";
136
138
  export declare const API_GRAPHS_MAP_EDGE_URL = "https://{url}/graphs/projects/{projectId}/maps/{mapId}/edges/{edgeId}";
139
+ export declare const API_GRAPHS_MAP_PATHS_URL = "https://{url}/graphs/projects/{projectId}/maps/{mapId}/paths";
140
+ export declare const API_GRAPHS_MAP_PATH_URL = "https://{url}/graphs/projects/{projectId}/maps/{mapId}/paths/{pathId}";
141
+ export declare const API_GRAPHS_MAP_PATH_APPEND_URL = "https://{url}/graphs/projects/{projectId}/maps/{mapId}/paths/{pathId}/append";
142
+ export declare const API_GRAPHS_MAP_PATH_APPEND_TRANSFORMABLE_URL = "https://{url}/graphs/projects/{projectId}/maps/{mapId}/paths/{pathId}/append/transformable";
137
143
  export declare const API_GRAPHS_MAPS_COPY_URL = "https://{url}/graphs/projects/{projectId}/maps/{mapId}/copy";
138
144
  export declare const API_GRAPHS_MAPS_DEPLOY_URL = "https://{url}/graphs/projects/{projectId}/maps/{mapId}/deploy";
139
145
  export declare const API_GRAPHS_MAPS_GEOJSON_URL = "https://{url}/graphs/projects/{projectId}/maps/{mapId}/geojson";
@@ -56,8 +56,12 @@ export const API_PROJECT_ROBOT_COMMAND2_URL = 'https://{url}/projects/{projectId
56
56
  export const API_PROJECT_ROBOT_BUTTON_URL = 'https://{url}/projects/{projectId}/robots/{callsign}/buttons';
57
57
  export const API_PROJECT_ROBOT_TRIGGER_URL = 'https://{url}/projects/{projectId}/robots/{callsign}/triggers';
58
58
  export const API_PROJECT_ROBOT_GAMEPAD_URL = 'https://{url}/projects/{projectId}/robots/{callsign}/gamepads';
59
- export const API_PROJECT_ROBOT_CONFIGS_CONNECTIONS_URL = 'https://{url}/robot-configs/connections/projects/{projectId}';
60
- export const API_PROJECT_ROBOT_CONFIGS_CONNECTION_URL = 'https://{url}/robot-configs/connections/projects/{projectId}/callsigns/{callsign}';
59
+ // Generic connections endpoint templates. `{service}` is the service's public path prefix
60
+ // (e.g. robot-configs, robot-services); the per-service specialisations below derive from these.
61
+ export const API_SERVICE_CONNECTIONS_URL = 'https://{url}/{service}/connections/projects/{projectId}';
62
+ export const API_SERVICE_CONNECTION_URL = 'https://{url}/{service}/connections/projects/{projectId}/callsigns/{callsign}';
63
+ export const API_PROJECT_ROBOT_CONFIGS_CONNECTIONS_URL = API_SERVICE_CONNECTIONS_URL.replace('{service}', 'robot-configs');
64
+ export const API_PROJECT_ROBOT_CONFIGS_CONNECTION_URL = API_SERVICE_CONNECTION_URL.replace('{service}', 'robot-configs');
61
65
  export const API_PROJECT_OPERATION_URL = 'https://{url}/projects/{projectId}/operations';
62
66
  export const API_PROJECT_OPERATION_ID_URL = 'https://{url}/projects/{projectId}/operations/{operationId}';
63
67
  export const API_PROJECT_DASHBOARD_URL = 'https://{url}/projects/{projectId}/dashboards';
@@ -134,6 +138,10 @@ export const API_GRAPHS_MAP_NODE_URL = 'https://{url}/graphs/projects/{projectId
134
138
  export const API_GRAPHS_MAP_EDGES_ADD_URL = 'https://{url}/graphs/projects/{projectId}/maps/{mapId}/edges/add';
135
139
  export const API_GRAPHS_MAP_EDGES_ADD_TRANSFORM_URL = 'https://{url}/graphs/projects/{projectId}/maps/{mapId}/edges/add/transform';
136
140
  export const API_GRAPHS_MAP_EDGE_URL = 'https://{url}/graphs/projects/{projectId}/maps/{mapId}/edges/{edgeId}';
141
+ export const API_GRAPHS_MAP_PATHS_URL = 'https://{url}/graphs/projects/{projectId}/maps/{mapId}/paths';
142
+ export const API_GRAPHS_MAP_PATH_URL = 'https://{url}/graphs/projects/{projectId}/maps/{mapId}/paths/{pathId}';
143
+ export const API_GRAPHS_MAP_PATH_APPEND_URL = 'https://{url}/graphs/projects/{projectId}/maps/{mapId}/paths/{pathId}/append';
144
+ export const API_GRAPHS_MAP_PATH_APPEND_TRANSFORMABLE_URL = 'https://{url}/graphs/projects/{projectId}/maps/{mapId}/paths/{pathId}/append/transformable';
137
145
  export const API_GRAPHS_MAPS_COPY_URL = 'https://{url}/graphs/projects/{projectId}/maps/{mapId}/copy';
138
146
  export const API_GRAPHS_MAPS_DEPLOY_URL = 'https://{url}/graphs/projects/{projectId}/maps/{mapId}/deploy';
139
147
  export const API_GRAPHS_MAPS_GEOJSON_URL = 'https://{url}/graphs/projects/{projectId}/maps/{mapId}/geojson';
@@ -51,6 +51,7 @@ export * from './maps/Map';
51
51
  export * from './maps/MapContent';
52
52
  export * from './maps/MapEdge';
53
53
  export * from './maps/MapNode';
54
+ export * from './maps/MapPath';
54
55
  export * from './maps/Panorama';
55
56
  export * from './message';
56
57
  export * from './params/ICallerParams';
@@ -51,6 +51,7 @@ export * from './maps/Map';
51
51
  export * from './maps/MapContent';
52
52
  export * from './maps/MapEdge';
53
53
  export * from './maps/MapNode';
54
+ export * from './maps/MapPath';
54
55
  export * from './maps/Panorama';
55
56
  export * from './message';
56
57
  export * from './params/ICallerParams';
@@ -0,0 +1,143 @@
1
+ import { Quaternion, Vector3 } from '../graph';
2
+ import { MapContentNode } from './MapContent';
3
+ /**
4
+ * A path (polyline or polygon) in a robot map, as returned by
5
+ * `GET /graphs/projects/{projectId}/maps/{mapId}/paths/{pathId}` — the same shape the
6
+ * rocos-agent map component's `paths/get` service reports.
7
+ *
8
+ * A path is stored as a sub-graph following the `go-common/graph/paths` convention: a parent
9
+ * node (type `path.parent.v1`, or any existing node acting as anchor) plus one member node
10
+ * per vertex, wired by `path.start.v1` / `path.belongs.v1` / `path.edge.v1` edges. The path
11
+ * id IS the parent node id.
12
+ */
13
+ export interface MapPath {
14
+ /** The path id (= parent node id). */
15
+ id: string;
16
+ /** Whether the path is a closed polygon (true) or an open polyline (false). */
17
+ closed: boolean;
18
+ /** The full parent node. */
19
+ parent: MapContentNode;
20
+ /** Ordered member (vertex) node ids, walked start → end. */
21
+ members: string[];
22
+ }
23
+ /** Filters for `MapService.listPaths`. */
24
+ export interface ListPathsFilter {
25
+ /** Only include paths with this closed flag (omit for both). */
26
+ closed?: boolean;
27
+ /**
28
+ * Only include paths whose parent node type is in this list (omit for all).
29
+ *
30
+ * Wire-format caveat: the server takes this as a single comma-separated query parameter
31
+ * (`?types=a,b`), so a node type that itself contains a comma cannot be expressed — it
32
+ * would be split into two wrong filter terms and silently match nothing. Node types are
33
+ * free-form text server-side, so this is conceivable if unlikely; avoid commas in type
34
+ * names, or filter client-side over `listPaths` + `getPath` results if you must.
35
+ */
36
+ types?: string[];
37
+ }
38
+ /**
39
+ * Body of `POST /graphs/projects/{projectId}/maps/{mapId}/paths/{pathId}/append`. Appends
40
+ * already-existing member nodes to a path, creating the path (and its `path.parent.v1`
41
+ * parent node) when absent — an existing node of any client-writable type is reused as the
42
+ * anchor instead.
43
+ *
44
+ * Members must already exist in the map (400 otherwise). `closed` is only applied while the
45
+ * path is empty; supplying a value that conflicts with a non-empty path's stored flag is
46
+ * rejected with 409 (use {@link MapService.replacePath} to change closed-ness). `data` keys
47
+ * are merged onto the parent node.
48
+ */
49
+ export interface AppendToPathRequest {
50
+ closed?: boolean;
51
+ data?: Record<string, unknown>;
52
+ members: string[];
53
+ }
54
+ /**
55
+ * A rigid-body transform positioning one path vertex relative to the frame named in the
56
+ * enclosing request. `rot` is optional and defaults to identity — path vertices are
57
+ * typically plain positions. Field naming matches {@link AffineTransform} (`pos`/`rot`);
58
+ * the service marshals onto the server's `translation`/`rotation` wire shape.
59
+ */
60
+ export interface PathPointTransform {
61
+ rot?: Quaternion;
62
+ pos: Vector3;
63
+ }
64
+ /**
65
+ * One vertex in a path payload. `id` is optional; when blank the server mints a UUID.
66
+ *
67
+ * `transform` is required by the append endpoint (which only creates vertices). In a path
68
+ * replace its presence carries the intent: supplied means the position is declarative (the
69
+ * vertex is created or — when the path exclusively owns the node — recreated at that
70
+ * position, which is how vertices are moved), while omitted means "reuse the existing node
71
+ * in place, position untouched".
72
+ */
73
+ export interface PathPoint {
74
+ id?: string;
75
+ data?: Record<string, unknown>;
76
+ transform?: PathPointTransform;
77
+ }
78
+ /**
79
+ * A {@link PathPoint} whose transform is mandatory, for the append/transformable body —
80
+ * that endpoint only creates vertices, so every point needs a position. Requiring it at the
81
+ * type level moves the server's 400 for a transform-less point to compile time.
82
+ */
83
+ export interface TransformablePathPoint extends PathPoint {
84
+ transform: PathPointTransform;
85
+ }
86
+ /**
87
+ * Body of `POST /graphs/projects/{projectId}/maps/{mapId}/paths/{pathId}/append/transformable`.
88
+ * For each point it creates a node (`nodeType`, default `FRAME`) positioned by a `TRANSFORM`
89
+ * edge from the existing `frame` node, and appends them all to the path in one transaction —
90
+ * the cloud analog of the rocos-agent's `paths/append/currentPosition`, with the caller
91
+ * supplying positions instead of the robot's live pose.
92
+ *
93
+ * Every point must carry a `transform` (enforced by {@link TransformablePathPoint}; the
94
+ * server also rejects a transform-less point with 400 — append only creates vertices; use
95
+ * {@link MapService.appendToPath} to append existing nodes). Node creation is create-only: a
96
+ * point id that already exists in the map is rejected with 409 and the whole append rolls
97
+ * back. `closed` and `data` behave as in {@link AppendToPathRequest}.
98
+ */
99
+ export interface AppendTransformablePointsRequest {
100
+ closed?: boolean;
101
+ data?: Record<string, unknown>;
102
+ frame: string;
103
+ nodeType?: string;
104
+ points: TransformablePathPoint[];
105
+ }
106
+ /** Response of the append/transformable endpoint: the created vertex ids, in path order. */
107
+ export interface AppendPathPointsResponse {
108
+ ids: string[];
109
+ }
110
+ /**
111
+ * Body of `PUT /graphs/projects/{projectId}/maps/{mapId}/paths/{pathId}` — the editor
112
+ * primitive. Declares the path's entire desired state; vertex move/insert/delete/reorder
113
+ * and closed-flag changes all reach the server as one idempotent call.
114
+ *
115
+ * The existing structure is torn down (old vertices exclusively owned by the path are
116
+ * deleted; shared members are detached, never moved or deleted; the parent node always
117
+ * survives) and rebuilt from `points`. Each point's `transform` carries its intent:
118
+ *
119
+ * - **With a transform** the position is declarative: the vertex is created (as `nodeType`,
120
+ * default `FRAME`, positioned by a `TRANSFORM` edge from `frame`), and for the path's own
121
+ * vertices same id + new transform = a move. A transform on a node the path doesn't
122
+ * exclusively own is rejected with 409.
123
+ * - **Without a transform** the point reuses an existing node in place, position untouched
124
+ * (400 if it doesn't exist) — how shared waypoints stay in the path.
125
+ *
126
+ * `parentType` applies when the parent node is created (default `path.parent.v1`); against
127
+ * an existing parent it must match (409) or be omitted (reused as-is, the anchored-path
128
+ * model). Replacing can also repair a structurally malformed path (which `getPath` reports
129
+ * as 422). Repeated identical PUTs are hash-stable: the server skips the map-timestamp bump
130
+ * and SUMMARY rewrite when the result matches what is stored.
131
+ *
132
+ * Access rules match {@link MapService.deletePath} (replace deletes vertices): the
133
+ * environment map is off-limits, the dronedeploy map is admin-only, normal maps are open to
134
+ * any caller with project access.
135
+ */
136
+ export interface ReplacePathRequest {
137
+ closed: boolean;
138
+ parentType?: string;
139
+ nodeType?: string;
140
+ data?: Record<string, unknown>;
141
+ frame?: string;
142
+ points: PathPoint[];
143
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -45,7 +45,7 @@ export declare enum ServiceConnection {
45
45
  * expected services.
46
46
  */
47
47
  export declare enum RobotConnectivity {
48
- /** Every expected service reports connected. */
48
+ /** At least one expected service is connected and none is disconnected. */
49
49
  ONLINE = "online",
50
50
  /** Some, but not all, expected services report connected. */
51
51
  DEGRADED = "degraded",
@@ -49,7 +49,7 @@ export var ServiceConnection;
49
49
  */
50
50
  export var RobotConnectivity;
51
51
  (function (RobotConnectivity) {
52
- /** Every expected service reports connected. */
52
+ /** At least one expected service is connected and none is disconnected. */
53
53
  RobotConnectivity["ONLINE"] = "online";
54
54
  /** Some, but not all, expected services report connected. */
55
55
  RobotConnectivity["DEGRADED"] = "degraded";
@@ -1,6 +1,7 @@
1
1
  import { AddEdgeRequest, AddEdgeResponse, AddTransformEdgeRequest } from '../models/maps/MapEdge';
2
2
  import { AddNodeRequest, AddNodeResponse, AddTransformableNodeRequest } from '../models/maps/MapNode';
3
3
  import { AddPanoramaRequest, AddPanoramaResponse, CreateObservation, Observation } from '../models/maps/Panorama';
4
+ import { AppendPathPointsResponse, AppendToPathRequest, AppendTransformablePointsRequest, ListPathsFilter, MapPath, ReplacePathRequest } from '../models/maps/MapPath';
4
5
  import { Asset, IBaseService, IRocosSDKConfig, Map, MapContent, RocosError } from '../models';
5
6
  import { BaseServiceAbstract } from './BaseServiceAbstract';
6
7
  /**
@@ -334,4 +335,104 @@ export declare class MapService extends BaseServiceAbstract implements IBaseServ
334
335
  * @param edgeId The ID of the edge to delete.
335
336
  */
336
337
  deleteEdge(projectId: string, mapId: string, edgeId: string): Promise<void>;
338
+ /**
339
+ * List the paths (polylines/polygons) in a map. A path is a sub-graph following the
340
+ * `go-common/graph/paths` convention shared with the rocos-agent's map component; this
341
+ * mirrors the agent's `paths/list` service, returning the parent node ids of every path
342
+ * that survives the filters.
343
+ *
344
+ * @param projectId The ID of the project the map belongs to.
345
+ * @param mapId The ID of the map to list paths from.
346
+ * @param filter Optional closed-flag and parent-node-type filters. The type filter goes
347
+ * over the wire as one comma-separated query parameter, so types containing a comma
348
+ * cannot be expressed (see {@link ListPathsFilter.types}).
349
+ * @returns The path ids (= parent node ids), sorted.
350
+ */
351
+ listPaths(projectId: string, mapId: string, filter?: ListPathsFilter): Promise<string[]>;
352
+ /**
353
+ * Get one path: its closed flag, full parent node, and ordered member node ids — the same
354
+ * shape as the agent's `paths/get` response. An existing node with no path structure is
355
+ * reported as an empty open path; a missing node is 404; stored structure violating the
356
+ * path convention (e.g. multiple start edges) is 422 (repair it with {@link replacePath}).
357
+ *
358
+ * @param projectId The ID of the project the map belongs to.
359
+ * @param mapId The ID of the map the path lives in.
360
+ * @param pathId The path id (= parent node id).
361
+ */
362
+ getPath(projectId: string, mapId: string, pathId: string): Promise<MapPath>;
363
+ /**
364
+ * Append already-existing member nodes to a path, creating the path when absent —
365
+ * mirroring the agent's `paths/append`. Members must already exist (400 otherwise);
366
+ * `path.index` numbering continues from the existing member count. A `closed` value
367
+ * conflicting with a non-empty path's stored flag is rejected with 409 (unlike the agent,
368
+ * which silently ignores it) — change closed-ness with {@link replacePath}.
369
+ *
370
+ * @param projectId The ID of the project the map belongs to.
371
+ * @param mapId The ID of the map the path lives in.
372
+ * @param pathId The path id (= parent node id).
373
+ * @param body The members to append, plus optional closed flag and parent data.
374
+ */
375
+ appendToPath(projectId: string, mapId: string, pathId: string, body: AppendToPathRequest): Promise<void>;
376
+ /**
377
+ * Create one positioned vertex node per point and append them all to a path in one
378
+ * transaction — the cloud analog of the agent's `paths/append/currentPosition`, with the
379
+ * caller supplying positions instead of the robot's live pose. Every point must carry a
380
+ * transform (append only creates; use {@link appendToPath} for existing nodes); a point id
381
+ * that already exists is a 409 and the whole append rolls back.
382
+ *
383
+ * Wire-shape note: the server receives each point's transform as
384
+ * `{ rotation, translation }`. This method marshals the caller-facing `rot`/`pos`
385
+ * (matching the node/panorama transform naming) onto that shape before POSTing.
386
+ *
387
+ * @param projectId The ID of the project the map belongs to.
388
+ * @param mapId The ID of the map the path lives in.
389
+ * @param pathId The path id (= parent node id).
390
+ * @param body The frame, points, and optional node type / closed flag / parent data.
391
+ * @returns The created vertex node ids, in path order (server-minted UUIDs for blank ids).
392
+ */
393
+ appendTransformablePointsToPath(projectId: string, mapId: string, pathId: string, body: AppendTransformablePointsRequest): Promise<AppendPathPointsResponse>;
394
+ /**
395
+ * Replace a path's entire structure in one idempotent call — the editor primitive for
396
+ * vertex move/insert/delete/reorder and closed-flag changes. See {@link ReplacePathRequest}
397
+ * for the full semantics; the essentials:
398
+ *
399
+ * - A point WITH a transform declares its position; for the path's own vertices, same id +
400
+ * new transform = a move. A transform on a node the path doesn't exclusively own is 409.
401
+ * - A point WITHOUT a transform reuses an existing node in place (400 if missing).
402
+ * - Old vertices exclusively owned by the path and not re-referenced are deleted; shared
403
+ * members are detached, never moved or deleted.
404
+ * - `parentType` must match an existing parent (409) or be omitted; it applies on create.
405
+ *
406
+ * @param projectId The ID of the project the map belongs to.
407
+ * @param mapId The ID of the map the path lives in.
408
+ * @param pathId The path id (= parent node id).
409
+ * @param body The path's desired state.
410
+ */
411
+ replacePath(projectId: string, mapId: string, pathId: string, body: ReplacePathRequest): Promise<void>;
412
+ /**
413
+ * Delete a path's structure, mirroring the agent's `paths/delete`: idempotent (a missing
414
+ * path still returns success), member nodes preserved by default, and the parent node
415
+ * itself deleted only when it is a pure `path.parent.v1` (an anchored path keeps its
416
+ * anchor node, minus the path edges).
417
+ *
418
+ * With `deleteMembers`, member nodes exclusively owned by the path (no remaining edges
419
+ * beyond their positioning `TRANSFORM`) are also removed; anything referenced elsewhere
420
+ * survives. Access rules match {@link deleteNode}: environment off-limits, dronedeploy
421
+ * admin-only, normal maps open to any caller with project access.
422
+ *
423
+ * @param projectId The ID of the project the map belongs to.
424
+ * @param mapId The ID of the map the path lives in.
425
+ * @param pathId The path id (= parent node id).
426
+ * @param opts Set `deleteMembers` to also remove exclusively-owned vertex nodes.
427
+ */
428
+ deletePath(projectId: string, mapId: string, pathId: string, opts?: {
429
+ deleteMembers?: boolean;
430
+ }): Promise<void>;
431
+ /**
432
+ * Marshal a caller-facing {@link PathPoint} onto the server's wire shape: `rot`/`pos`
433
+ * become `rotation`/`translation`, and an absent transform stays absent (its presence
434
+ * carries intent in a replace). A missing `rot` is left for the server to default to
435
+ * identity.
436
+ */
437
+ private pathPointToWire;
337
438
  }