@totemsdk/act-mavlink 0.0.0-stage → 0.1.0
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/LICENSE +202 -0
- package/NOTICE +60 -0
- package/README.md +108 -2
- package/dist/adapter.d.ts +22 -0
- package/dist/adapter.js +403 -0
- package/dist/cjs/adapter.d.ts +22 -0
- package/dist/cjs/adapter.js +406 -0
- package/dist/cjs/conformance.d.ts +28 -0
- package/dist/cjs/conformance.js +122 -0
- package/dist/cjs/effects.d.ts +16 -0
- package/dist/cjs/effects.js +19 -0
- package/dist/cjs/envelope.d.ts +38 -0
- package/dist/cjs/envelope.js +98 -0
- package/dist/cjs/index.d.ts +19 -0
- package/dist/cjs/index.js +31 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/profile.d.ts +36 -0
- package/dist/cjs/profile.js +167 -0
- package/dist/cjs/transport.d.ts +56 -0
- package/dist/cjs/transport.js +33 -0
- package/dist/conformance.d.ts +28 -0
- package/dist/conformance.js +119 -0
- package/dist/effects.d.ts +16 -0
- package/dist/effects.js +16 -0
- package/dist/envelope.d.ts +38 -0
- package/dist/envelope.js +92 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +13 -0
- package/dist/profile.d.ts +36 -0
- package/dist/profile.js +159 -0
- package/dist/transport.d.ts +56 -0
- package/dist/transport.js +28 -0
- package/package.json +72 -3
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-025 §5.5 — mission envelope.
|
|
3
|
+
*
|
|
4
|
+
* The envelope is the unit of authority: `uploadMission` prepares and commits
|
|
5
|
+
* it, `startMission` is authorized against its digest, and RFC-023 enforces its
|
|
6
|
+
* speed/duration/altitude bounds from the prepared command. A start that
|
|
7
|
+
* references a different plan than the one authorized fails closed.
|
|
8
|
+
*/
|
|
9
|
+
export interface Waypoint {
|
|
10
|
+
latitudeDeg: number;
|
|
11
|
+
longitudeDeg: number;
|
|
12
|
+
altitudeMetres: number;
|
|
13
|
+
}
|
|
14
|
+
export interface Geofence {
|
|
15
|
+
latitudeDeg: number;
|
|
16
|
+
longitudeDeg: number;
|
|
17
|
+
radiusMetres: number;
|
|
18
|
+
}
|
|
19
|
+
export interface MissionEnvelope {
|
|
20
|
+
envelopeId: string;
|
|
21
|
+
/** ResourceId key of the target vehicle. */
|
|
22
|
+
vehicle: string;
|
|
23
|
+
waypoints: readonly Waypoint[];
|
|
24
|
+
/** Altitude ceiling, in metres (decimal string). */
|
|
25
|
+
maxAltitudeMetres: string;
|
|
26
|
+
/** Ground-speed ceiling, in m/s (decimal string). */
|
|
27
|
+
maxSpeedMetresPerSecond: string;
|
|
28
|
+
/** Mission wall-clock ceiling, in seconds (decimal string). */
|
|
29
|
+
maxDurationSeconds: string;
|
|
30
|
+
geofence?: Geofence;
|
|
31
|
+
}
|
|
32
|
+
/** Great-circle distance between two WGS-84 points, in metres. */
|
|
33
|
+
export declare function distanceMetres(a: Waypoint, b: Waypoint): number;
|
|
34
|
+
/** Canonical digest of the authorized plan. */
|
|
35
|
+
export declare function envelopeDigest(envelope: MissionEnvelope): string;
|
|
36
|
+
/** Structural and physical validation; returns human-readable issues. */
|
|
37
|
+
export declare function validateMissionEnvelope(envelope: MissionEnvelope): string[];
|
|
38
|
+
export declare function assertValidMissionEnvelope(envelope: MissionEnvelope): void;
|
package/dist/envelope.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-025 §5.5 — mission envelope.
|
|
3
|
+
*
|
|
4
|
+
* The envelope is the unit of authority: `uploadMission` prepares and commits
|
|
5
|
+
* it, `startMission` is authorized against its digest, and RFC-023 enforces its
|
|
6
|
+
* speed/duration/altitude bounds from the prepared command. A start that
|
|
7
|
+
* references a different plan than the one authorized fails closed.
|
|
8
|
+
*/
|
|
9
|
+
import { hashCanonical } from '@totemsdk/act-core';
|
|
10
|
+
const EARTH_RADIUS_METRES = 6371000;
|
|
11
|
+
/** Great-circle distance between two WGS-84 points, in metres. */
|
|
12
|
+
export function distanceMetres(a, b) {
|
|
13
|
+
const toRad = (deg) => (deg * Math.PI) / 180;
|
|
14
|
+
const dLat = toRad(b.latitudeDeg - a.latitudeDeg);
|
|
15
|
+
const dLon = toRad(b.longitudeDeg - a.longitudeDeg);
|
|
16
|
+
const lat1 = toRad(a.latitudeDeg);
|
|
17
|
+
const lat2 = toRad(b.latitudeDeg);
|
|
18
|
+
const h = Math.sin(dLat / 2) ** 2 +
|
|
19
|
+
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
|
|
20
|
+
return 2 * EARTH_RADIUS_METRES * Math.asin(Math.min(1, Math.sqrt(h)));
|
|
21
|
+
}
|
|
22
|
+
/** Canonical digest of the authorized plan. */
|
|
23
|
+
export function envelopeDigest(envelope) {
|
|
24
|
+
return hashCanonical('TOTEM_ACT_MAVLINK_MISSION_ENVELOPE_V1', {
|
|
25
|
+
envelopeId: envelope.envelopeId,
|
|
26
|
+
vehicle: envelope.vehicle,
|
|
27
|
+
waypoints: envelope.waypoints.map((waypoint) => ({
|
|
28
|
+
latitudeDeg: waypoint.latitudeDeg,
|
|
29
|
+
longitudeDeg: waypoint.longitudeDeg,
|
|
30
|
+
altitudeMetres: waypoint.altitudeMetres,
|
|
31
|
+
})),
|
|
32
|
+
maxAltitudeMetres: envelope.maxAltitudeMetres,
|
|
33
|
+
maxSpeedMetresPerSecond: envelope.maxSpeedMetresPerSecond,
|
|
34
|
+
maxDurationSeconds: envelope.maxDurationSeconds,
|
|
35
|
+
geofence: envelope.geofence
|
|
36
|
+
? {
|
|
37
|
+
latitudeDeg: envelope.geofence.latitudeDeg,
|
|
38
|
+
longitudeDeg: envelope.geofence.longitudeDeg,
|
|
39
|
+
radiusMetres: envelope.geofence.radiusMetres,
|
|
40
|
+
}
|
|
41
|
+
: null,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function positive(value) {
|
|
45
|
+
return Number.isFinite(Number(value)) && Number(value) > 0;
|
|
46
|
+
}
|
|
47
|
+
/** Structural and physical validation; returns human-readable issues. */
|
|
48
|
+
export function validateMissionEnvelope(envelope) {
|
|
49
|
+
const issues = [];
|
|
50
|
+
if (envelope.envelopeId.trim() === '')
|
|
51
|
+
issues.push('envelopeId is required');
|
|
52
|
+
if (envelope.vehicle.trim() === '')
|
|
53
|
+
issues.push('vehicle is required');
|
|
54
|
+
if (envelope.waypoints.length === 0)
|
|
55
|
+
issues.push('at least one waypoint is required');
|
|
56
|
+
if (envelope.waypoints.length > 100)
|
|
57
|
+
issues.push('mission exceeds the 100-waypoint limit');
|
|
58
|
+
if (!positive(envelope.maxAltitudeMetres))
|
|
59
|
+
issues.push('maxAltitudeMetres must be positive');
|
|
60
|
+
if (!positive(envelope.maxSpeedMetresPerSecond))
|
|
61
|
+
issues.push('maxSpeedMetresPerSecond must be positive');
|
|
62
|
+
if (!positive(envelope.maxDurationSeconds))
|
|
63
|
+
issues.push('maxDurationSeconds must be positive');
|
|
64
|
+
const maxAltitude = Number(envelope.maxAltitudeMetres);
|
|
65
|
+
for (const [index, waypoint] of envelope.waypoints.entries()) {
|
|
66
|
+
if (!Number.isFinite(waypoint.latitudeDeg) || waypoint.latitudeDeg < -90 || waypoint.latitudeDeg > 90) {
|
|
67
|
+
issues.push(`waypoint ${index} latitude is out of range`);
|
|
68
|
+
}
|
|
69
|
+
if (!Number.isFinite(waypoint.longitudeDeg) || waypoint.longitudeDeg < -180 || waypoint.longitudeDeg > 180) {
|
|
70
|
+
issues.push(`waypoint ${index} longitude is out of range`);
|
|
71
|
+
}
|
|
72
|
+
if (!Number.isFinite(waypoint.altitudeMetres) || waypoint.altitudeMetres < 0) {
|
|
73
|
+
issues.push(`waypoint ${index} altitude is invalid`);
|
|
74
|
+
}
|
|
75
|
+
else if (waypoint.altitudeMetres > maxAltitude) {
|
|
76
|
+
issues.push(`waypoint ${index} altitude ${waypoint.altitudeMetres} m exceeds the envelope ceiling ${maxAltitude} m`);
|
|
77
|
+
}
|
|
78
|
+
if (envelope.geofence) {
|
|
79
|
+
const distance = distanceMetres({ latitudeDeg: envelope.geofence.latitudeDeg, longitudeDeg: envelope.geofence.longitudeDeg, altitudeMetres: 0 }, waypoint);
|
|
80
|
+
if (distance > envelope.geofence.radiusMetres) {
|
|
81
|
+
issues.push(`waypoint ${index} is ${Math.round(distance)} m from the geofence centre (limit ${envelope.geofence.radiusMetres} m)`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return issues;
|
|
86
|
+
}
|
|
87
|
+
export function assertValidMissionEnvelope(envelope) {
|
|
88
|
+
const issues = validateMissionEnvelope(envelope);
|
|
89
|
+
if (issues.length > 0) {
|
|
90
|
+
throw new Error(`invalid mission envelope '${envelope.envelopeId}': ${issues.join('; ')}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-025 — governed MAVLink / autonomous flight profile.
|
|
3
|
+
*
|
|
4
|
+
* `@totemsdk/act-mavlink` authorizes a bounded mission envelope; the connector
|
|
5
|
+
* (`@totemsdk/edge-mavlink`) carries the wire frames and the autopilot owns
|
|
6
|
+
* stabilization, failsafes and the real-time loop.
|
|
7
|
+
*/
|
|
8
|
+
export { MavlinkTransportError, assertEnvelopeBinding, } from './transport.js';
|
|
9
|
+
export type { MavlinkTransportPort, MissionStartRequest, MissionUploadRequest, MissionUploadResult, TelemetrySnapshot, } from './transport.js';
|
|
10
|
+
export { assertValidMissionEnvelope, distanceMetres, envelopeDigest, validateMissionEnvelope, } from './envelope.js';
|
|
11
|
+
export type { Geofence, MissionEnvelope, Waypoint } from './envelope.js';
|
|
12
|
+
export { deriveFlightEffects } from './effects.js';
|
|
13
|
+
export type { FlightLimits } from './effects.js';
|
|
14
|
+
export { MAVLINK_PROTOCOL, createMavlinkProfile, mavlinkErrorTaxonomy, vehicleInterlocks, vehicleResource, vehicleResourceId, } from './profile.js';
|
|
15
|
+
export type { FlightVehicle, MavlinkProfileOptions } from './profile.js';
|
|
16
|
+
export { createMavlinkDefinitions } from './adapter.js';
|
|
17
|
+
export type { MavlinkAdapterOptions } from './adapter.js';
|
|
18
|
+
export { runMavlinkConformance } from './conformance.js';
|
|
19
|
+
export type { MavlinkConformanceCheck, MavlinkConformanceOptions, MavlinkConformanceReport, } from './conformance.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-025 — governed MAVLink / autonomous flight profile.
|
|
3
|
+
*
|
|
4
|
+
* `@totemsdk/act-mavlink` authorizes a bounded mission envelope; the connector
|
|
5
|
+
* (`@totemsdk/edge-mavlink`) carries the wire frames and the autopilot owns
|
|
6
|
+
* stabilization, failsafes and the real-time loop.
|
|
7
|
+
*/
|
|
8
|
+
export { MavlinkTransportError, assertEnvelopeBinding, } from './transport.js';
|
|
9
|
+
export { assertValidMissionEnvelope, distanceMetres, envelopeDigest, validateMissionEnvelope, } from './envelope.js';
|
|
10
|
+
export { deriveFlightEffects } from './effects.js';
|
|
11
|
+
export { MAVLINK_PROTOCOL, createMavlinkProfile, mavlinkErrorTaxonomy, vehicleInterlocks, vehicleResource, vehicleResourceId, } from './profile.js';
|
|
12
|
+
export { createMavlinkDefinitions } from './adapter.js';
|
|
13
|
+
export { runMavlinkConformance } from './conformance.js';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-025 §5.4 / §5.9 — MAVLink flight profile.
|
|
3
|
+
*
|
|
4
|
+
* The vehicle is an asset addressed at its autopilot. Safe state is
|
|
5
|
+
* return-to-launch (a `fail-safe` in flight must never command a de-energize
|
|
6
|
+
* analogue). Interlocks enforce the vehicle's declared ceilings before
|
|
7
|
+
* authorization; safety-coded errors require the safe state and never retry.
|
|
8
|
+
*/
|
|
9
|
+
import { type DeviceErrorTaxonomy, type IndustrialProfile, type Interlock, type Resource, type ResourceId } from '@totemsdk/act-core';
|
|
10
|
+
import { type MissionEnvelope } from './envelope.js';
|
|
11
|
+
export declare const MAVLINK_PROTOCOL = "mavlink";
|
|
12
|
+
export interface FlightVehicle {
|
|
13
|
+
vehicleId: string;
|
|
14
|
+
/** Ground-speed ceiling, in m/s (decimal string). */
|
|
15
|
+
maxSpeedMetresPerSecond: string;
|
|
16
|
+
/** Altitude ceiling, in metres (decimal string). */
|
|
17
|
+
maxAltitudeMetres: string;
|
|
18
|
+
/** Mission duration ceiling, in seconds (decimal string). */
|
|
19
|
+
maxDurationSeconds: string;
|
|
20
|
+
/** MAVLink addressing. */
|
|
21
|
+
systemId?: number;
|
|
22
|
+
componentId?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function vehicleResourceId(site: string, vehicleId: string): ResourceId;
|
|
25
|
+
export declare function vehicleResource(site: string, vehicle: FlightVehicle): Resource;
|
|
26
|
+
/** Interlocks enforcing the vehicle's declared envelope ceilings. */
|
|
27
|
+
export declare function vehicleInterlocks(site: string, vehicle: FlightVehicle): Interlock[];
|
|
28
|
+
/** MAVLink result codes → device error classification (RFC-011 §4.7). */
|
|
29
|
+
export declare function mavlinkErrorTaxonomy(): DeviceErrorTaxonomy;
|
|
30
|
+
export interface MavlinkProfileOptions {
|
|
31
|
+
site: string;
|
|
32
|
+
vehicles: readonly FlightVehicle[];
|
|
33
|
+
}
|
|
34
|
+
/** Data-only flight profile (definitions are supplied by the adapter). */
|
|
35
|
+
export declare function createMavlinkProfile(options: MavlinkProfileOptions): IndustrialProfile;
|
|
36
|
+
export type { MissionEnvelope };
|
package/dist/profile.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-025 §5.4 / §5.9 — MAVLink flight profile.
|
|
3
|
+
*
|
|
4
|
+
* The vehicle is an asset addressed at its autopilot. Safe state is
|
|
5
|
+
* return-to-launch (a `fail-safe` in flight must never command a de-energize
|
|
6
|
+
* analogue). Interlocks enforce the vehicle's declared ceilings before
|
|
7
|
+
* authorization; safety-coded errors require the safe state and never retry.
|
|
8
|
+
*/
|
|
9
|
+
import { createDeviceErrorTaxonomy, } from '@totemsdk/act-core';
|
|
10
|
+
import { distanceMetres } from './envelope.js';
|
|
11
|
+
export const MAVLINK_PROTOCOL = 'mavlink';
|
|
12
|
+
export function vehicleResourceId(site, vehicleId) {
|
|
13
|
+
return { site, area: 'flight', asset: vehicleId, point: 'autopilot' };
|
|
14
|
+
}
|
|
15
|
+
export function vehicleResource(site, vehicle) {
|
|
16
|
+
const id = vehicleResourceId(site, vehicle.vehicleId);
|
|
17
|
+
return {
|
|
18
|
+
id,
|
|
19
|
+
kind: 'controller',
|
|
20
|
+
addresses: [
|
|
21
|
+
{
|
|
22
|
+
protocol: MAVLINK_PROTOCOL,
|
|
23
|
+
endpoint: `system:${vehicle.systemId ?? 1}/component:${vehicle.componentId ?? 1}`,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
// Hold/land/return are deployment policy; return-to-launch is the default
|
|
27
|
+
// safe state. Never de-energize an airborne vehicle.
|
|
28
|
+
safeState: {
|
|
29
|
+
resourceId: id,
|
|
30
|
+
command: { action: 'flight:returnToLaunch' },
|
|
31
|
+
},
|
|
32
|
+
metadata: {
|
|
33
|
+
vehicleId: vehicle.vehicleId,
|
|
34
|
+
maxSpeedMetresPerSecond: vehicle.maxSpeedMetresPerSecond,
|
|
35
|
+
maxAltitudeMetres: vehicle.maxAltitudeMetres,
|
|
36
|
+
maxDurationSeconds: vehicle.maxDurationSeconds,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function numeric(value) {
|
|
41
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
42
|
+
return value;
|
|
43
|
+
if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value))) {
|
|
44
|
+
return Number(value);
|
|
45
|
+
}
|
|
46
|
+
if (value && typeof value === 'object' && 'value' in value) {
|
|
47
|
+
return numeric(value.value);
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
function ceilingInterlock(site, vehicle, id, parameter, ceiling, unit) {
|
|
52
|
+
const resourceId = vehicleResourceId(site, vehicle.vehicleId);
|
|
53
|
+
return {
|
|
54
|
+
id,
|
|
55
|
+
kind: 'precondition',
|
|
56
|
+
resources: [resourceId],
|
|
57
|
+
safeState: { resourceId, command: { action: 'flight:returnToLaunch' } },
|
|
58
|
+
evaluate({ parameters }) {
|
|
59
|
+
const requested = numeric(parameters[parameter]);
|
|
60
|
+
if (requested === undefined)
|
|
61
|
+
return { satisfied: true };
|
|
62
|
+
return requested <= Number(ceiling)
|
|
63
|
+
? { satisfied: true }
|
|
64
|
+
: { satisfied: false, reason: `requested ${requested} ${unit} exceeds vehicle ceiling ${ceiling} ${unit}` };
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Interlocks enforcing the vehicle's declared envelope ceilings. */
|
|
69
|
+
export function vehicleInterlocks(site, vehicle) {
|
|
70
|
+
const resourceId = vehicleResourceId(site, vehicle.vehicleId);
|
|
71
|
+
const interlocks = [
|
|
72
|
+
ceilingInterlock(site, vehicle, `mavlink:speed-ceiling:${vehicle.vehicleId}`, 'maxSpeedMetresPerSecond', vehicle.maxSpeedMetresPerSecond, 'm/s'),
|
|
73
|
+
ceilingInterlock(site, vehicle, `mavlink:altitude-ceiling:${vehicle.vehicleId}`, 'maxAltitudeMetres', vehicle.maxAltitudeMetres, 'm'),
|
|
74
|
+
ceilingInterlock(site, vehicle, `mavlink:duration-ceiling:${vehicle.vehicleId}`, 'maxDurationSeconds', vehicle.maxDurationSeconds, 's'),
|
|
75
|
+
];
|
|
76
|
+
// Geofence containment is dynamic: it depends on the waypoints in the
|
|
77
|
+
// prepared request, not only on scalar ceilings.
|
|
78
|
+
interlocks.push({
|
|
79
|
+
id: `mavlink:geofence:${vehicle.vehicleId}`,
|
|
80
|
+
kind: 'precondition',
|
|
81
|
+
resources: [resourceId],
|
|
82
|
+
safeState: { resourceId, command: { action: 'flight:returnToLaunch' } },
|
|
83
|
+
evaluate({ parameters }) {
|
|
84
|
+
const latitude = numeric(parameters.geofenceLatitudeDeg);
|
|
85
|
+
const longitude = numeric(parameters.geofenceLongitudeDeg);
|
|
86
|
+
const radius = numeric(parameters.geofenceRadiusMetres);
|
|
87
|
+
if (latitude === undefined || longitude === undefined || radius === undefined) {
|
|
88
|
+
return { satisfied: true };
|
|
89
|
+
}
|
|
90
|
+
const waypoints = parameters.waypoints;
|
|
91
|
+
if (!Array.isArray(waypoints))
|
|
92
|
+
return { satisfied: true };
|
|
93
|
+
for (const [index, waypoint] of waypoints.entries()) {
|
|
94
|
+
const candidate = waypoint;
|
|
95
|
+
const distance = distanceMetres({ latitudeDeg: latitude, longitudeDeg: longitude, altitudeMetres: 0 }, candidate);
|
|
96
|
+
if (distance > radius) {
|
|
97
|
+
return {
|
|
98
|
+
satisfied: false,
|
|
99
|
+
reason: `waypoint ${index} is ${Math.round(distance)} m from the geofence centre (limit ${radius} m)`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { satisfied: true };
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
return interlocks;
|
|
107
|
+
}
|
|
108
|
+
/** MAVLink result codes → device error classification (RFC-011 §4.7). */
|
|
109
|
+
export function mavlinkErrorTaxonomy() {
|
|
110
|
+
const taxonomy = createDeviceErrorTaxonomy();
|
|
111
|
+
const classes = {
|
|
112
|
+
// Failsafe/geofence conditions require the safe state and never retry.
|
|
113
|
+
GEOFENCE_BREACH: 'safety',
|
|
114
|
+
FAILSAFE_TRIGGERED: 'safety',
|
|
115
|
+
BATTERY_LOW: 'safety',
|
|
116
|
+
CRITICAL_BATTERY: 'safety',
|
|
117
|
+
// Mission/protocol rejection — retrying the same plan cannot succeed.
|
|
118
|
+
MAV_MISSION_ERROR: 'permanent',
|
|
119
|
+
MAV_MISSION_UNSUPPORTED: 'permanent',
|
|
120
|
+
MAV_MISSION_UNSUPPORTED_FRAME: 'permanent',
|
|
121
|
+
MAV_MISSION_NO_SPACE: 'permanent',
|
|
122
|
+
MAV_MISSION_INVALID: 'permanent',
|
|
123
|
+
MAV_MISSION_INVALID_PARAM1: 'permanent',
|
|
124
|
+
MAV_MISSION_INVALID_PARAM2: 'permanent',
|
|
125
|
+
MAV_MISSION_INVALID_PARAM3: 'permanent',
|
|
126
|
+
MAV_MISSION_INVALID_PARAM4: 'permanent',
|
|
127
|
+
MAV_MISSION_INVALID_PARAM5_X: 'permanent',
|
|
128
|
+
MAV_MISSION_INVALID_PARAM6_Y: 'permanent',
|
|
129
|
+
MAV_MISSION_INVALID_PARAM7: 'permanent',
|
|
130
|
+
MAV_MISSION_DENIED: 'auth',
|
|
131
|
+
MAV_RESULT_UNSUPPORTED: 'permanent',
|
|
132
|
+
MAV_RESULT_FAILED: 'permanent',
|
|
133
|
+
MAV_RESULT_CANCELLED: 'permanent',
|
|
134
|
+
// Link/transport — retry under the same operation id is safe.
|
|
135
|
+
LINK_TIMEOUT: 'transient',
|
|
136
|
+
MAV_RESULT_IN_PROGRESS: 'transient',
|
|
137
|
+
TIMEOUT: 'transient',
|
|
138
|
+
// Authorization.
|
|
139
|
+
AUTHORIZATION_DENIED: 'auth',
|
|
140
|
+
ENVELOPE_MISMATCH: 'auth',
|
|
141
|
+
};
|
|
142
|
+
for (const [code, classification] of Object.entries(classes)) {
|
|
143
|
+
taxonomy.register(MAVLINK_PROTOCOL, code, classification);
|
|
144
|
+
}
|
|
145
|
+
return taxonomy;
|
|
146
|
+
}
|
|
147
|
+
/** Data-only flight profile (definitions are supplied by the adapter). */
|
|
148
|
+
export function createMavlinkProfile(options) {
|
|
149
|
+
return {
|
|
150
|
+
id: 'mavlink',
|
|
151
|
+
version: 1,
|
|
152
|
+
resources: options.vehicles.map((vehicle) => vehicleResource(options.site, vehicle)),
|
|
153
|
+
interlocks: options.vehicles.flatMap((vehicle) => vehicleInterlocks(options.site, vehicle)),
|
|
154
|
+
errorTaxonomy: mavlinkErrorTaxonomy(),
|
|
155
|
+
capabilities: ['industrial:action', 'industrial:read', 'transport:mavlink'],
|
|
156
|
+
recipes: [],
|
|
157
|
+
definitions: [],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-025 §5.2 — MAVLink transport port.
|
|
3
|
+
*
|
|
4
|
+
* The caller injects a MAVLink client (SDK, bridge, or simulator). This package
|
|
5
|
+
* owns mission authority, bounded execution and evidence; the autopilot owns
|
|
6
|
+
* stabilization, failsafes and the real-time loop.
|
|
7
|
+
*
|
|
8
|
+
* `startMission` carries the authorized envelope digest so a transport can
|
|
9
|
+
* refuse to start a mission that differs from the one that was authorized.
|
|
10
|
+
*/
|
|
11
|
+
import { type MissionEnvelope, type Waypoint } from './envelope.js';
|
|
12
|
+
export interface MissionUploadRequest {
|
|
13
|
+
envelopeId: string;
|
|
14
|
+
envelopeDigest: string;
|
|
15
|
+
waypoints: readonly Waypoint[];
|
|
16
|
+
}
|
|
17
|
+
export interface MissionUploadResult {
|
|
18
|
+
/** Autopilot-assigned mission id (the reconciliation handle). */
|
|
19
|
+
missionId: string;
|
|
20
|
+
}
|
|
21
|
+
export interface MissionStartRequest {
|
|
22
|
+
missionId: string;
|
|
23
|
+
envelopeDigest: string;
|
|
24
|
+
}
|
|
25
|
+
export interface TelemetrySnapshot {
|
|
26
|
+
/** ISO-8601 timestamp from the autopilot. */
|
|
27
|
+
timestamp: string;
|
|
28
|
+
latitudeDeg: number;
|
|
29
|
+
longitudeDeg: number;
|
|
30
|
+
altitudeMetres: number;
|
|
31
|
+
groundspeedMetresPerSecond: number;
|
|
32
|
+
batteryPercent?: number;
|
|
33
|
+
waypointIndex?: number;
|
|
34
|
+
missionFinished?: boolean;
|
|
35
|
+
armed?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface MavlinkTransportPort {
|
|
38
|
+
uploadMission(request: MissionUploadRequest): Promise<MissionUploadResult>;
|
|
39
|
+
startMission(request: MissionStartRequest): Promise<void>;
|
|
40
|
+
hold(): Promise<void>;
|
|
41
|
+
returnToLaunch(): Promise<void>;
|
|
42
|
+
land(): Promise<void>;
|
|
43
|
+
/** Telemetry readback used for reconciliation; must originate from the vehicle. */
|
|
44
|
+
readTelemetry(): Promise<TelemetrySnapshot>;
|
|
45
|
+
/** Optional push path for telemetry. */
|
|
46
|
+
onTelemetry?(handler: (telemetry: TelemetrySnapshot) => void): () => void;
|
|
47
|
+
}
|
|
48
|
+
export declare class MavlinkTransportError extends Error {
|
|
49
|
+
readonly code: string;
|
|
50
|
+
constructor(code: string, message: string);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A start must reference the exact plan that was authorized. A digest that does
|
|
54
|
+
* not match the envelope fails closed before dispatch.
|
|
55
|
+
*/
|
|
56
|
+
export declare function assertEnvelopeBinding(envelope: MissionEnvelope, digest: string): void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-025 §5.2 — MAVLink transport port.
|
|
3
|
+
*
|
|
4
|
+
* The caller injects a MAVLink client (SDK, bridge, or simulator). This package
|
|
5
|
+
* owns mission authority, bounded execution and evidence; the autopilot owns
|
|
6
|
+
* stabilization, failsafes and the real-time loop.
|
|
7
|
+
*
|
|
8
|
+
* `startMission` carries the authorized envelope digest so a transport can
|
|
9
|
+
* refuse to start a mission that differs from the one that was authorized.
|
|
10
|
+
*/
|
|
11
|
+
import { envelopeDigest } from './envelope.js';
|
|
12
|
+
export class MavlinkTransportError extends Error {
|
|
13
|
+
constructor(code, message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'MavlinkTransportError';
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A start must reference the exact plan that was authorized. A digest that does
|
|
21
|
+
* not match the envelope fails closed before dispatch.
|
|
22
|
+
*/
|
|
23
|
+
export function assertEnvelopeBinding(envelope, digest) {
|
|
24
|
+
const expected = envelopeDigest(envelope);
|
|
25
|
+
if (digest !== expected) {
|
|
26
|
+
throw new MavlinkTransportError('EnvelopeMismatch', `envelope digest mismatch for '${envelope.envelopeId}' (authorized ${digest}, prepared ${expected})`);
|
|
27
|
+
}
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,75 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totemsdk/act-mavlink",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Governed MAVLink autonomous-flight profile for Totem — bounded mission envelopes, telemetry reconciliation and conformance on the act-core runtime",
|
|
5
|
+
"main": "dist/cjs/index.js",
|
|
6
|
+
"types": "dist/cjs/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"require": {
|
|
14
|
+
"types": "./dist/cjs/index.d.ts",
|
|
15
|
+
"default": "./dist/cjs/index.js"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"NOTICE"
|
|
24
|
+
],
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@totemsdk/act-core": "^0.5.0",
|
|
27
|
+
"@totemsdk/agent-policy": "^0.4.0",
|
|
28
|
+
"@totemsdk/edge": "^1.5.1"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/jest": "^30.0.0",
|
|
32
|
+
"@types/node": "^20.0.0",
|
|
33
|
+
"jest": "^30.4.2",
|
|
34
|
+
"jest-environment-node": "^30.0.0",
|
|
35
|
+
"jest-runtime": "^30.5.2",
|
|
36
|
+
"ts-jest": "^29.0.0",
|
|
37
|
+
"typescript": "^7.0.2"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"license": "Apache-2.0",
|
|
46
|
+
"author": "Totem SDK",
|
|
47
|
+
"homepage": "https://docs.totem.ing",
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/Totem-Edge/totem-sdk/issues"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/Totem-Edge/totem-sdk.git",
|
|
54
|
+
"directory": "packages/act-mavlink"
|
|
55
|
+
},
|
|
56
|
+
"keywords": [
|
|
57
|
+
"totem",
|
|
58
|
+
"totemsdk",
|
|
59
|
+
"mavlink",
|
|
60
|
+
"drone",
|
|
61
|
+
"flight",
|
|
62
|
+
"mission",
|
|
63
|
+
"geofence",
|
|
64
|
+
"governed-action",
|
|
65
|
+
"act-core"
|
|
66
|
+
],
|
|
67
|
+
"type": "module",
|
|
68
|
+
"scripts": {
|
|
69
|
+
"build": "tsc && tsc -p tsconfig.cjs.json && node -e \"require('fs').writeFileSync('dist/cjs/package.json', JSON.stringify({type:'commonjs'})+'\\n')\"",
|
|
70
|
+
"clean": "rm -rf dist",
|
|
71
|
+
"test": "jest --config ../../tests/jest/act-mavlink.cjs",
|
|
72
|
+
"test:integration": "node --test ../../tests/qualification/act-mavlink/integration.test.mjs",
|
|
73
|
+
"lint": "eslint src --max-warnings 0"
|
|
74
|
+
}
|
|
6
75
|
}
|