tassello 0.5.0 → 0.7.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/CHANGELOG.md +52 -0
- package/README.md +130 -10
- package/dist/server/actions.d.ts +24 -0
- package/dist/server/actions.js +331 -0
- package/dist/server/behavior_tree_playback.js +2 -2
- package/dist/server/behavior_trees.d.ts +5 -3
- package/dist/server/behavior_trees.js +33 -35
- package/dist/server/files.d.ts +1 -1
- package/dist/server/files.js +4 -1
- package/dist/server/interfaces.d.ts +16 -0
- package/dist/server/interfaces.js +145 -0
- package/dist/server/message_code.d.ts +12 -0
- package/dist/server/message_code.js +165 -0
- package/dist/server/message_format.d.ts +24 -0
- package/dist/server/message_format.js +345 -0
- package/dist/server/overlay_encoding.d.ts +26 -0
- package/dist/server/overlay_encoding.js +40 -0
- package/dist/server/overlays.d.ts +21 -0
- package/dist/server/overlays.js +172 -0
- package/dist/server/param_format.d.ts +33 -0
- package/dist/server/param_format.js +155 -0
- package/dist/server/param_yaml.d.ts +14 -0
- package/dist/server/param_yaml.js +168 -0
- package/dist/server/parameters.d.ts +38 -0
- package/dist/server/parameters.js +299 -0
- package/dist/server/ros.d.ts +4 -2
- package/dist/server/ros.js +13 -5
- package/dist/server/ros_graph.d.ts +25 -0
- package/dist/server/ros_graph.js +148 -0
- package/dist/server/ros_graph_format.d.ts +51 -0
- package/dist/server/ros_graph_format.js +139 -0
- package/dist/server/rosbag.d.ts +15 -11
- package/dist/server/rosbag.js +396 -144
- package/dist/server/rosbag_format.d.ts +28 -0
- package/dist/server/rosbag_format.js +366 -0
- package/dist/server/rosbag_process.d.ts +20 -0
- package/dist/server/rosbag_process.js +99 -0
- package/dist/server/rosbags.d.ts +42 -0
- package/dist/server/rosbags.js +444 -0
- package/dist/server/server.js +31 -3
- package/dist/server/session.d.ts +16 -0
- package/dist/server/session.js +127 -2
- package/dist/server/tf.d.ts +6 -0
- package/dist/server/tf.js +19 -1
- package/dist/server/topic_monitor.d.ts +25 -0
- package/dist/server/topic_monitor.js +229 -0
- package/dist/server/topic_publishing.d.ts +20 -0
- package/dist/server/topic_publishing.js +153 -0
- package/dist/server/topic_stats.d.ts +13 -0
- package/dist/server/topic_stats.js +106 -0
- package/dist/server/topics.js +2 -1
- package/dist/server/transforms.d.ts +1 -0
- package/dist/server/transforms.js +5 -0
- package/dist/shared/monitor.d.ts +144 -0
- package/dist/shared/monitor.js +12 -0
- package/dist/shared/parameters.d.ts +29 -0
- package/dist/shared/parameters.js +16 -0
- package/dist/shared/protocol.d.ts +156 -2
- package/dist/shared/protocol.js +2 -0
- package/dist/shared/rosbag.d.ts +104 -0
- package/dist/shared/rosbag.js +35 -0
- package/dist/types/client/api.d.ts +1 -1
- package/dist/types/client/connection.d.ts +60 -3
- package/dist/types/client/icons.d.ts +6 -0
- package/dist/types/client/sdk/index.d.ts +5 -2
- package/dist/types/shared/monitor.d.ts +144 -0
- package/dist/types/shared/parameters.d.ts +29 -0
- package/dist/types/shared/protocol.d.ts +156 -2
- package/dist/types/shared/rosbag.d.ts +104 -0
- package/dist/www/assets/Joystick.js +1 -1
- package/dist/www/assets/RobotModelView.js +2 -2
- package/dist/www/assets/TerminalView.js +2 -2
- package/dist/www/assets/dialogs.js +1 -1
- package/dist/www/assets/icons.js +1 -1
- package/dist/www/assets/index.css +1 -1
- package/dist/www/assets/index.js +47 -45
- package/dist/www/assets/shared/tassello-sdk.js +1 -1
- package/docs/plugins.md +15 -2
- package/package.json +1 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type rclnodejs from 'rclnodejs';
|
|
2
2
|
import { type BehaviorTreeRecording, type BehaviorTreeUpdate } from '../shared/protocol.ts';
|
|
3
|
+
import { type RecordOptions } from '../shared/rosbag.ts';
|
|
3
4
|
import { type TreeInfo } from './behavior_tree_file.ts';
|
|
4
5
|
import { BehaviorTreePlayback, type PlaybackWatcher } from './behavior_tree_playback.ts';
|
|
5
6
|
import type { StreamState } from './frame_stream.ts';
|
|
7
|
+
import type { Rosbags } from './rosbags.ts';
|
|
6
8
|
import { type Logger } from './util.ts';
|
|
7
9
|
export type RecordingInfo = BehaviorTreeRecording['recording'];
|
|
8
10
|
export interface BehaviorTreeWatcher {
|
|
@@ -17,12 +19,12 @@ export declare class BehaviorTrees {
|
|
|
17
19
|
private readonly uploadsDir;
|
|
18
20
|
private readonly topics;
|
|
19
21
|
private readonly monitors;
|
|
22
|
+
private readonly rosbags;
|
|
20
23
|
private readonly recordings;
|
|
21
|
-
constructor(node: rclnodejs.Node, logger: Logger, uploadsDir: string);
|
|
24
|
+
constructor(node: rclnodejs.Node, logger: Logger, uploadsDir: string, rosbags: Rosbags);
|
|
22
25
|
watch(topic: string, tree: string, watcher: BehaviorTreeWatcher): () => void;
|
|
23
|
-
record(topic: string, path: string | null): Promise<string | null>;
|
|
26
|
+
record(topic: string, path: string | null, options?: RecordOptions): Promise<string | null>;
|
|
24
27
|
play(bag: string, topic: string, tree: string, watcher: PlaybackWatcher): BehaviorTreePlayback;
|
|
25
|
-
stopAll(): Promise<void>;
|
|
26
28
|
private recordingOf;
|
|
27
29
|
private announce;
|
|
28
30
|
private forget;
|
|
@@ -9,17 +9,19 @@
|
|
|
9
9
|
// outlives its last page for LINGER_MS: a page reloaded, or a panel shown
|
|
10
10
|
// again, finds the nodes as they are, which the log only says when they change.
|
|
11
11
|
//
|
|
12
|
-
// A topic is recorded into a rosbag
|
|
13
|
-
//
|
|
14
|
-
//
|
|
12
|
+
// A topic is recorded into a rosbag when a page asks, until one asks to stop
|
|
13
|
+
// (or a Rosbags widget stops it): every page following it is told. The
|
|
14
|
+
// recording is the server's rosbags' (rosbags.ts), as the Rosbags widgets'
|
|
15
|
+
// are. A bag is played back for the page that opens it
|
|
16
|
+
// (behavior_tree_playback.ts).
|
|
15
17
|
import { promises as fs } from 'node:fs';
|
|
16
18
|
import { BT_RATE } from '../shared/protocol.js';
|
|
19
|
+
import { DEFAULT_RECORD_OPTIONS } from '../shared/rosbag.js';
|
|
17
20
|
import { readTree } from './behavior_tree_file.js';
|
|
18
21
|
import { logTransitions } from './behavior_tree_log.js';
|
|
19
22
|
import { BehaviorTreePlayback } from './behavior_tree_playback.js';
|
|
20
23
|
import { BehaviorTreeState } from './behavior_tree_state.js';
|
|
21
24
|
import { publishersQos } from './qos.js';
|
|
22
|
-
import { BagRecorder, newBagFolder } from './rosbag.js';
|
|
23
25
|
import { absoluteTopic, errorMessage } from './util.js';
|
|
24
26
|
// How often the graph is checked for the topic's publishers, until it has one
|
|
25
27
|
const RETRY_PERIOD_MS = 1000;
|
|
@@ -252,12 +254,23 @@ export class BehaviorTrees {
|
|
|
252
254
|
topics = new Map();
|
|
253
255
|
// By topic, then tree source
|
|
254
256
|
monitors = new Map();
|
|
255
|
-
|
|
257
|
+
rosbags;
|
|
258
|
+
// By topic: the folder it is recorded into, '' while it starts
|
|
256
259
|
recordings = new Map();
|
|
257
|
-
constructor(node, logger, uploadsDir) {
|
|
260
|
+
constructor(node, logger, uploadsDir, rosbags) {
|
|
258
261
|
this.node = node;
|
|
259
262
|
this.logger = logger;
|
|
260
263
|
this.uploadsDir = uploadsDir;
|
|
264
|
+
this.rosbags = rosbags;
|
|
265
|
+
// A recording stopped elsewhere, or by itself
|
|
266
|
+
rosbags.onChange(() => {
|
|
267
|
+
for (const [topic, folder] of this.recordings) {
|
|
268
|
+
if (folder && !rosbags.recording(folder)) {
|
|
269
|
+
this.recordings.delete(topic);
|
|
270
|
+
this.announce(topic);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
});
|
|
261
274
|
}
|
|
262
275
|
// Until the returned function is called
|
|
263
276
|
watch(topic, tree, watcher) {
|
|
@@ -290,57 +303,42 @@ export class BehaviorTrees {
|
|
|
290
303
|
}
|
|
291
304
|
// Records the topic into a new folder, or stops (path null): resolves with
|
|
292
305
|
// the bag's folder, null if it was not recording
|
|
293
|
-
async record(topic, path) {
|
|
306
|
+
async record(topic, path, options = DEFAULT_RECORD_OPTIONS) {
|
|
294
307
|
const name = absoluteTopic(topic);
|
|
295
308
|
const current = this.recordings.get(name);
|
|
296
309
|
if (path === null) {
|
|
297
|
-
if (!current
|
|
310
|
+
if (!current) {
|
|
298
311
|
return null;
|
|
299
312
|
}
|
|
300
313
|
this.recordings.delete(name);
|
|
301
314
|
this.announce(name);
|
|
302
|
-
|
|
303
|
-
this.logger.info(`Recorded ${name} into ${current.path}`);
|
|
304
|
-
return current.path;
|
|
315
|
+
return this.rosbags.stop(current);
|
|
305
316
|
}
|
|
306
|
-
if (current) {
|
|
307
|
-
throw new Error(`${name} is being recorded already
|
|
317
|
+
if (current !== undefined) {
|
|
318
|
+
throw new Error(`${name} is being recorded already${current ? `, into ${current}` : ''}`);
|
|
308
319
|
}
|
|
309
|
-
|
|
310
|
-
|
|
320
|
+
this.recordings.set(name, '');
|
|
321
|
+
let folder;
|
|
311
322
|
try {
|
|
312
|
-
|
|
313
|
-
|
|
323
|
+
const request = { path, topics: [name], all: false, regex: '', exclude: '', options };
|
|
324
|
+
folder = (await this.rosbags.record(request, 'behavior-tree')).path;
|
|
314
325
|
}
|
|
315
326
|
catch (e) {
|
|
316
327
|
this.recordings.delete(name);
|
|
317
328
|
throw new Error(`Cannot record ${name}: ${errorMessage(e)}`);
|
|
318
329
|
}
|
|
319
|
-
|
|
320
|
-
recording.recorder.onExit = (reason) => {
|
|
321
|
-
if (this.recordings.get(name) === recording) {
|
|
322
|
-
this.recordings.delete(name);
|
|
323
|
-
this.announce(name);
|
|
324
|
-
this.logger.warn(`Recording ${name} stopped: ${reason}`);
|
|
325
|
-
}
|
|
326
|
-
};
|
|
327
|
-
this.logger.info(`Recording ${name} into ${recording.path}`);
|
|
330
|
+
this.recordings.set(name, folder);
|
|
328
331
|
this.announce(name);
|
|
329
|
-
return
|
|
332
|
+
return folder;
|
|
330
333
|
}
|
|
331
334
|
// A bag played back for a page, until closed
|
|
332
335
|
play(bag, topic, tree, watcher) {
|
|
333
336
|
return new BehaviorTreePlayback({ bag, topic, tree: tree.trim(), uploadsDir: this.uploadsDir, logger: this.logger }, watcher);
|
|
334
337
|
}
|
|
335
|
-
// The recordings completed, as the server stops
|
|
336
|
-
async stopAll() {
|
|
337
|
-
const recorders = [...this.recordings.values()].flatMap((recording) => recording.recorder ?? []);
|
|
338
|
-
this.recordings.clear();
|
|
339
|
-
await Promise.all(recorders.map((recorder) => recorder.stop()));
|
|
340
|
-
}
|
|
341
338
|
recordingOf(topic) {
|
|
342
|
-
const
|
|
343
|
-
|
|
339
|
+
const folder = this.recordings.get(topic);
|
|
340
|
+
const recording = folder ? this.rosbags.recording(folder) : null;
|
|
341
|
+
return recording ? { path: recording.path, since: recording.since } : null;
|
|
344
342
|
}
|
|
345
343
|
announce(topic) {
|
|
346
344
|
for (const monitor of this.monitors.get(topic)?.values() ?? []) {
|
package/dist/server/files.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const IMAGE_EXTENSIONS: Set<string>;
|
|
2
|
-
export type FileKind = 'image' | 'behavior-tree' | 'bag' | 'folder';
|
|
2
|
+
export type FileKind = 'image' | 'behavior-tree' | 'bag' | 'yaml' | 'folder';
|
|
3
3
|
export declare const MAX_UPLOAD_BYTES: number;
|
|
4
4
|
export declare function fileKind(value: string | null): FileKind;
|
|
5
5
|
export declare function resolveImagePath(input: string, home?: string): string;
|
package/dist/server/files.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// behavior tree inspectors: files of the robot, which a page picks by
|
|
3
3
|
// browsing its directories, and files uploaded from a page, kept by the
|
|
4
4
|
// server next to the panels. Only those: the dashboard does not hand out any
|
|
5
|
-
// file of the robot. A page also browses for rosbags,
|
|
5
|
+
// file of the robot. A page also browses for rosbags, parameter files (YAML),
|
|
6
|
+
// and folders.
|
|
6
7
|
import { createHash } from 'node:crypto';
|
|
7
8
|
import { promises as fs } from 'node:fs';
|
|
8
9
|
import os from 'node:os';
|
|
@@ -13,6 +14,8 @@ const EXTENSIONS = {
|
|
|
13
14
|
image: IMAGE_EXTENSIONS,
|
|
14
15
|
'behavior-tree': new Set(['.xml']),
|
|
15
16
|
bag: new Set(['.mcap', '.db3']),
|
|
17
|
+
// Parameter files
|
|
18
|
+
yaml: new Set(['.yaml', '.yml']),
|
|
16
19
|
folder: new Set(),
|
|
17
20
|
};
|
|
18
21
|
const FILE_KINDS = new Set(Object.keys(EXTENSIONS));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import rclnodejs from 'rclnodejs';
|
|
2
|
+
import type { InterfaceInfo, MessageSchema } from '../shared/monitor.ts';
|
|
3
|
+
import { type RosTime } from './message_format.ts';
|
|
4
|
+
export declare class Interfaces {
|
|
5
|
+
private readonly node;
|
|
6
|
+
private readonly schemas;
|
|
7
|
+
private readonly infos;
|
|
8
|
+
constructor(node: rclnodejs.Node);
|
|
9
|
+
schemasOf(type: string, into?: Record<string, MessageSchema>): Record<string, MessageSchema>;
|
|
10
|
+
info(type: string): InterfaceInfo;
|
|
11
|
+
message(type: string, value: unknown): Record<string, unknown>;
|
|
12
|
+
hasHeader(type: string): boolean;
|
|
13
|
+
now(): RosTime;
|
|
14
|
+
private schema;
|
|
15
|
+
private definition;
|
|
16
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// The ROS interfaces (messages, services, actions) for the monitor widgets:
|
|
2
|
+
// their definitions as ros2 interface show prints them, from the files of
|
|
3
|
+
// their packages; their schemas and default values, from rclnodejs's code of
|
|
4
|
+
// them; and the JSON of the pages made into their messages.
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import rclnodejs from 'rclnodejs';
|
|
8
|
+
import { defaultMessage, expandDefinition, HEADER_TYPE, parseType, partTypes, toRosMessage, } from './message_format.js';
|
|
9
|
+
import { missingCodeHint } from './message_code.js';
|
|
10
|
+
import { errorMessage, packageShareDirectory } from './util.js';
|
|
11
|
+
function load(type) {
|
|
12
|
+
let typeClass;
|
|
13
|
+
try {
|
|
14
|
+
typeClass = rclnodejs.require(type);
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
throw new Error(missingCodeHint(errorMessage(e)));
|
|
18
|
+
}
|
|
19
|
+
if (!typeClass?.ROSMessageDef) {
|
|
20
|
+
throw new Error(`${type} is not a message type`);
|
|
21
|
+
}
|
|
22
|
+
return typeClass;
|
|
23
|
+
}
|
|
24
|
+
// A field's message type, pkg/<msg|srv|action>/Name: rclnodejs tells only
|
|
25
|
+
// its package and name, which the parts of services and actions share
|
|
26
|
+
function nestedType(pkg, name) {
|
|
27
|
+
const kinds = /_(Request|Response|Event)$/.test(name)
|
|
28
|
+
? ['srv', 'msg', 'action']
|
|
29
|
+
: /_(Goal|Result|Feedback|FeedbackMessage|SendGoal_Request|SendGoal_Response|GetResult_Request|GetResult_Response)$/.test(name)
|
|
30
|
+
? ['action', 'msg', 'srv']
|
|
31
|
+
: ['msg', 'action', 'srv'];
|
|
32
|
+
for (const kind of kinds) {
|
|
33
|
+
try {
|
|
34
|
+
if (rclnodejs.require(`${pkg}/${kind}/${name}`)?.ROSMessageDef) {
|
|
35
|
+
return `${pkg}/${kind}/${name}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Not of this kind
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return `${pkg}/msg/${name}`;
|
|
43
|
+
}
|
|
44
|
+
function plain(value) {
|
|
45
|
+
if (ArrayBuffer.isView(value)) {
|
|
46
|
+
return Array.from(value, (v) => (typeof v === 'bigint' ? Number(v) : v));
|
|
47
|
+
}
|
|
48
|
+
return typeof value === 'bigint' ? Number(value) : value;
|
|
49
|
+
}
|
|
50
|
+
export class Interfaces {
|
|
51
|
+
node;
|
|
52
|
+
schemas = new Map();
|
|
53
|
+
infos = new Map();
|
|
54
|
+
constructor(node) {
|
|
55
|
+
this.node = node;
|
|
56
|
+
}
|
|
57
|
+
// The schema of a message type, and of those it nests
|
|
58
|
+
schemasOf(type, into = {}) {
|
|
59
|
+
if (into[type]) {
|
|
60
|
+
return into;
|
|
61
|
+
}
|
|
62
|
+
const schema = this.schema(type);
|
|
63
|
+
into[type] = schema;
|
|
64
|
+
for (const field of schema.fields) {
|
|
65
|
+
if (!field.primitive) {
|
|
66
|
+
this.schemasOf(field.type, into);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return into;
|
|
70
|
+
}
|
|
71
|
+
// ros2 interface show, and the schemas and default values of its parts
|
|
72
|
+
info(type) {
|
|
73
|
+
const known = this.infos.get(type);
|
|
74
|
+
if (known) {
|
|
75
|
+
return known;
|
|
76
|
+
}
|
|
77
|
+
const { pkg, kind, name } = parseType(type);
|
|
78
|
+
const schemas = {};
|
|
79
|
+
const parts = partTypes(type).map((part) => {
|
|
80
|
+
this.schemasOf(part.type, schemas);
|
|
81
|
+
return { ...part, default: defaultMessage(part.type, schemas) };
|
|
82
|
+
});
|
|
83
|
+
const info = { type, kind, text: this.definition(pkg, kind, name), parts, schemas };
|
|
84
|
+
this.infos.set(type, info);
|
|
85
|
+
return info;
|
|
86
|
+
}
|
|
87
|
+
// The message of type the page's JSON describes (see toRosMessage)
|
|
88
|
+
message(type, value) {
|
|
89
|
+
return toRosMessage(value, type, this.schemasOf(type), () => this.now());
|
|
90
|
+
}
|
|
91
|
+
// Its messages begin with a std_msgs/msg/Header, whose stamp ros2 topic
|
|
92
|
+
// delay compares with their arrival
|
|
93
|
+
hasHeader(type) {
|
|
94
|
+
try {
|
|
95
|
+
const first = this.schema(type).fields[0];
|
|
96
|
+
return first?.name === 'header' && first.type === HEADER_TYPE;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// On the ROS clock: the simulation's with use_sim_time
|
|
103
|
+
now() {
|
|
104
|
+
const { seconds, nanoseconds } = this.node.getClock().now().secondsAndNanoseconds;
|
|
105
|
+
return { sec: Number(seconds), nanosec: Number(nanoseconds) };
|
|
106
|
+
}
|
|
107
|
+
schema(type) {
|
|
108
|
+
const known = this.schemas.get(type);
|
|
109
|
+
if (known) {
|
|
110
|
+
return known;
|
|
111
|
+
}
|
|
112
|
+
const { ROSMessageDef: def } = load(type);
|
|
113
|
+
const schema = {
|
|
114
|
+
fields: def.fields.map(({ name, type: t, default_value }) => ({
|
|
115
|
+
name,
|
|
116
|
+
type: t.isPrimitiveType ? t.type : nestedType(t.pkgName ?? '', t.type),
|
|
117
|
+
primitive: t.isPrimitiveType,
|
|
118
|
+
array: !t.isArray ? 'none' : t.isFixedSizeArray ? 'fixed' : t.isUpperBound ? 'bounded' : 'dynamic',
|
|
119
|
+
size: t.isArray && (t.isFixedSizeArray || t.isUpperBound) ? t.arraySize : null,
|
|
120
|
+
stringBound: t.stringUpperBound ?? null,
|
|
121
|
+
default: plain(default_value) ?? null,
|
|
122
|
+
})),
|
|
123
|
+
constants: def.constants.map(({ name, type: t, value }) => ({ name, type: t, value: plain(value) })),
|
|
124
|
+
};
|
|
125
|
+
this.schemas.set(type, schema);
|
|
126
|
+
return schema;
|
|
127
|
+
}
|
|
128
|
+
// Its file's text, expanded; null if not found
|
|
129
|
+
definition(pkg, kind, name) {
|
|
130
|
+
const text = readDefinition(pkg, kind, name);
|
|
131
|
+
return text === null ? null : expandDefinition(text, pkg, (p, n) => readDefinition(p, 'msg', n));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function readDefinition(pkg, kind, name) {
|
|
135
|
+
const share = packageShareDirectory(pkg);
|
|
136
|
+
if (!share) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
return readFileSync(path.join(share, kind, `${name}.${kind}`), 'utf8');
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Logger } from './util.ts';
|
|
2
|
+
export interface MissingPackage {
|
|
3
|
+
name: string;
|
|
4
|
+
prefix: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function expectedFiles(pkg: string, index: string, blocked?: (name: string) => boolean): string[];
|
|
7
|
+
export declare function missingPackages(prefixes: string[], generatedRoot: string, blocklist?: {
|
|
8
|
+
pkgName?: string;
|
|
9
|
+
interfaceName?: string;
|
|
10
|
+
}[]): Promise<MissingPackage[]>;
|
|
11
|
+
export declare function missingCodeHint(message: string): string;
|
|
12
|
+
export declare function makeMessageCode(logger: Logger): Promise<void>;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// rclnodejs's JavaScript code of the ROS interfaces, without which it cannot
|
|
2
|
+
// use them: rclnodejs makes it when installed, for the packages sourced then.
|
|
3
|
+
// Those sourced since (a robot's own installation, a workspace), or rebuilt
|
|
4
|
+
// with new interfaces, are made here at start, before anything uses them: by
|
|
5
|
+
// rclnodejs's own generator, as it makes them when installed, run on a prefix
|
|
6
|
+
// of their own (their ament index entries, their share folders linked). Its
|
|
7
|
+
// own fallback misses the interfaces of services and actions, and makes a
|
|
8
|
+
// whole prefix's while the server waits.
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import { existsSync, promises as fs } from 'node:fs';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { errorMessage, tasselloRoot } from './util.js';
|
|
15
|
+
// Where the ament index lists each package's interfaces
|
|
16
|
+
const INDEX = path.join('share', 'ament_index', 'resource_index', 'rosidl_interfaces');
|
|
17
|
+
const GENERATION_TIMEOUT_MS = 5 * 60_000;
|
|
18
|
+
// The files rclnodejs makes of a package's interfaces, from its entry in the
|
|
19
|
+
// ament index: msg/Pose.msg, msg/Pose.idl give <pkg>__msg__Pose.js. The
|
|
20
|
+
// services' and actions' parts are made with them.
|
|
21
|
+
export function expectedFiles(pkg, index, blocked = () => false) {
|
|
22
|
+
const files = new Set();
|
|
23
|
+
for (const line of index.split('\n')) {
|
|
24
|
+
const match = /^([\w-]+)\/(\w+)\.(msg|srv|action|idl)$/.exec(line.trim());
|
|
25
|
+
if (match && !blocked(match[2])) {
|
|
26
|
+
// msg-common: msg, as rclnodejs names them
|
|
27
|
+
files.add(`${pkg}__${match[1].split('-')[0]}__${match[2]}.js`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return [...files];
|
|
31
|
+
}
|
|
32
|
+
// The packages of the prefixes whose code is not all made: the first of each
|
|
33
|
+
// name, as the prefixes overlay those after them
|
|
34
|
+
export async function missingPackages(prefixes, generatedRoot, blocklist = []) {
|
|
35
|
+
const seen = new Set();
|
|
36
|
+
const missing = [];
|
|
37
|
+
const filters = blocklist.map((f) => ({
|
|
38
|
+
pkg: f.pkgName ? new RegExp(f.pkgName) : null,
|
|
39
|
+
name: f.interfaceName ? new RegExp(f.interfaceName) : null,
|
|
40
|
+
}));
|
|
41
|
+
for (const prefix of prefixes) {
|
|
42
|
+
let packages;
|
|
43
|
+
try {
|
|
44
|
+
packages = await fs.readdir(path.join(prefix, INDEX));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
for (const name of packages.sort()) {
|
|
50
|
+
if (seen.has(name)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
seen.add(name);
|
|
54
|
+
const applying = filters.filter((f) => !f.pkg || f.pkg.test(name));
|
|
55
|
+
if (applying.some((f) => !f.name)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
let index;
|
|
59
|
+
try {
|
|
60
|
+
index = await fs.readFile(path.join(prefix, INDEX, name), 'utf8');
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const expected = expectedFiles(name, index, (interfaceName) => applying.some((f) => f.name?.test(interfaceName)));
|
|
66
|
+
const made = new Set(await fs.readdir(path.join(generatedRoot, name)).catch(() => []));
|
|
67
|
+
if (expected.some((file) => !made.has(file))) {
|
|
68
|
+
missing.push({ name, prefix });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return missing;
|
|
73
|
+
}
|
|
74
|
+
// What to do when rclnodejs has no code of an interface, after its error
|
|
75
|
+
export function missingCodeHint(message) {
|
|
76
|
+
return /message required does not exist/i.test(message)
|
|
77
|
+
? `${message}. Tassello makes the code of the interfaces sourced when it starts: restart it with their workspace sourced, or run npx generate-ros-messages in ${tasselloRoot()}`
|
|
78
|
+
: message;
|
|
79
|
+
}
|
|
80
|
+
function rclnodejsRoot() {
|
|
81
|
+
return path.dirname(createRequire(import.meta.url).resolve('rclnodejs/package.json'));
|
|
82
|
+
}
|
|
83
|
+
// Runs rclnodejs's generator on the prefix: rejects with its error output if
|
|
84
|
+
// it fails
|
|
85
|
+
function generate(worker, prefix) {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
const child = spawn(process.execPath, [worker], {
|
|
88
|
+
env: { ...process.env, WORKER_TARGET_PATH: prefix },
|
|
89
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
90
|
+
});
|
|
91
|
+
let stderr = '';
|
|
92
|
+
child.stderr.on('data', (data) => {
|
|
93
|
+
stderr = (stderr + data.toString()).slice(-2000);
|
|
94
|
+
});
|
|
95
|
+
const timer = setTimeout(() => child.kill('SIGKILL'), GENERATION_TIMEOUT_MS);
|
|
96
|
+
child.on('error', (e) => {
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
reject(e);
|
|
99
|
+
});
|
|
100
|
+
child.on('close', (code, signal) => {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
if (code === 0) {
|
|
103
|
+
resolve();
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
reject(new Error(stderr.trim() || (signal ? `killed by ${signal}` : `exit code ${code}`)));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// Makes the code of the packages sourced whose code is missing; says what it
|
|
112
|
+
// did, or why it could not, and goes on: those packages stay unusable then
|
|
113
|
+
export async function makeMessageCode(logger) {
|
|
114
|
+
const prefixes = (process.env.AMENT_PREFIX_PATH ?? '').split(path.delimiter).filter(Boolean);
|
|
115
|
+
let root;
|
|
116
|
+
try {
|
|
117
|
+
root = rclnodejsRoot();
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
logger.warn(`Cannot find rclnodejs to check its code of the ROS interfaces: ${errorMessage(e)}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const generatedRoot = path.join(root, 'generated');
|
|
124
|
+
let blocklist = [];
|
|
125
|
+
try {
|
|
126
|
+
blocklist = JSON.parse(await fs.readFile(path.join(root, 'rosidl_gen', 'blocklist.json'), 'utf8'));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// None
|
|
130
|
+
}
|
|
131
|
+
const missing = await missingPackages(prefixes, generatedRoot, blocklist);
|
|
132
|
+
if (missing.length === 0) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const names = missing.map((p) => p.name).join(', ');
|
|
136
|
+
const worker = path.join(root, 'rosidl_gen', 'generate_worker.cjs');
|
|
137
|
+
if (!existsSync(worker)) {
|
|
138
|
+
logger.warn(`rclnodejs has no code of the interfaces of ${names}: run npx generate-ros-messages in ${tasselloRoot()} with them sourced`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
logger.info(`Making rclnodejs's code of the ROS interfaces it has not, of ${missing.length} package(s): ${names}`);
|
|
142
|
+
const started = Date.now();
|
|
143
|
+
const prefix = await fs.mkdtemp(path.join(os.tmpdir(), 'tassello-interfaces-'));
|
|
144
|
+
try {
|
|
145
|
+
await fs.mkdir(path.join(prefix, INDEX), { recursive: true });
|
|
146
|
+
for (const pkg of missing) {
|
|
147
|
+
await fs.copyFile(path.join(pkg.prefix, INDEX, pkg.name), path.join(prefix, INDEX, pkg.name));
|
|
148
|
+
await fs.symlink(path.join(pkg.prefix, 'share', pkg.name), path.join(prefix, 'share', pkg.name));
|
|
149
|
+
}
|
|
150
|
+
await generate(worker, prefix);
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
logger.warn(`Could not make rclnodejs's code of the interfaces of ${names} (${errorMessage(e)}): run npx generate-ros-messages in ${tasselloRoot()} with them sourced, as a user who may write in ${generatedRoot}`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
await fs.rm(prefix, { recursive: true, force: true });
|
|
158
|
+
}
|
|
159
|
+
const left = await missingPackages(missing.map((p) => p.prefix).filter((p, i, all) => all.indexOf(p) === i), generatedRoot, blocklist);
|
|
160
|
+
const still = left.filter((p) => missing.some((m) => m.name === p.name));
|
|
161
|
+
if (still.length > 0) {
|
|
162
|
+
logger.warn(`rclnodejs's code of some interfaces of ${still.map((p) => p.name).join(', ')} could not be made`);
|
|
163
|
+
}
|
|
164
|
+
logger.info(`Made in ${((Date.now() - started) / 1000).toFixed(1)} s`);
|
|
165
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { InterfaceKind, MessageSchema } from '../shared/monitor.ts';
|
|
2
|
+
export declare const TIME_TYPE = "builtin_interfaces/msg/Time";
|
|
3
|
+
export declare const HEADER_TYPE = "std_msgs/msg/Header";
|
|
4
|
+
export declare function isPrimitive(type: string): boolean;
|
|
5
|
+
export declare function parseType(type: string): {
|
|
6
|
+
pkg: string;
|
|
7
|
+
kind: InterfaceKind;
|
|
8
|
+
name: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function partTypes(type: string): {
|
|
11
|
+
name: string;
|
|
12
|
+
type: string;
|
|
13
|
+
}[];
|
|
14
|
+
export type DefinitionReader = (pkg: string, name: string) => string | null;
|
|
15
|
+
export declare function nestedFieldType(line: string, pkg: string): [string, string] | null;
|
|
16
|
+
export declare function expandDefinition(text: string, pkg: string, read: DefinitionReader, depth?: number): string;
|
|
17
|
+
export declare function defaultMessage(type: string, schemas: Record<string, MessageSchema>): Record<string, unknown>;
|
|
18
|
+
export interface RosTime {
|
|
19
|
+
sec: number;
|
|
20
|
+
nanosec: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function toRosMessage(value: unknown, type: string, schemas: Record<string, MessageSchema>, now: () => RosTime, path?: string): Record<string, unknown>;
|
|
23
|
+
export declare function fieldAt(message: unknown, path: string): unknown;
|
|
24
|
+
export declare function echoJson(value: unknown, limit: number): unknown;
|