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.
Files changed (79) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +130 -10
  3. package/dist/server/actions.d.ts +24 -0
  4. package/dist/server/actions.js +331 -0
  5. package/dist/server/behavior_tree_playback.js +2 -2
  6. package/dist/server/behavior_trees.d.ts +5 -3
  7. package/dist/server/behavior_trees.js +33 -35
  8. package/dist/server/files.d.ts +1 -1
  9. package/dist/server/files.js +4 -1
  10. package/dist/server/interfaces.d.ts +16 -0
  11. package/dist/server/interfaces.js +145 -0
  12. package/dist/server/message_code.d.ts +12 -0
  13. package/dist/server/message_code.js +165 -0
  14. package/dist/server/message_format.d.ts +24 -0
  15. package/dist/server/message_format.js +345 -0
  16. package/dist/server/overlay_encoding.d.ts +26 -0
  17. package/dist/server/overlay_encoding.js +40 -0
  18. package/dist/server/overlays.d.ts +21 -0
  19. package/dist/server/overlays.js +172 -0
  20. package/dist/server/param_format.d.ts +33 -0
  21. package/dist/server/param_format.js +155 -0
  22. package/dist/server/param_yaml.d.ts +14 -0
  23. package/dist/server/param_yaml.js +168 -0
  24. package/dist/server/parameters.d.ts +38 -0
  25. package/dist/server/parameters.js +299 -0
  26. package/dist/server/ros.d.ts +4 -2
  27. package/dist/server/ros.js +13 -5
  28. package/dist/server/ros_graph.d.ts +25 -0
  29. package/dist/server/ros_graph.js +148 -0
  30. package/dist/server/ros_graph_format.d.ts +51 -0
  31. package/dist/server/ros_graph_format.js +139 -0
  32. package/dist/server/rosbag.d.ts +15 -11
  33. package/dist/server/rosbag.js +396 -144
  34. package/dist/server/rosbag_format.d.ts +28 -0
  35. package/dist/server/rosbag_format.js +366 -0
  36. package/dist/server/rosbag_process.d.ts +20 -0
  37. package/dist/server/rosbag_process.js +99 -0
  38. package/dist/server/rosbags.d.ts +42 -0
  39. package/dist/server/rosbags.js +444 -0
  40. package/dist/server/server.js +31 -3
  41. package/dist/server/session.d.ts +16 -0
  42. package/dist/server/session.js +127 -2
  43. package/dist/server/tf.d.ts +6 -0
  44. package/dist/server/tf.js +19 -1
  45. package/dist/server/topic_monitor.d.ts +25 -0
  46. package/dist/server/topic_monitor.js +229 -0
  47. package/dist/server/topic_publishing.d.ts +20 -0
  48. package/dist/server/topic_publishing.js +153 -0
  49. package/dist/server/topic_stats.d.ts +13 -0
  50. package/dist/server/topic_stats.js +106 -0
  51. package/dist/server/topics.js +2 -1
  52. package/dist/server/transforms.d.ts +1 -0
  53. package/dist/server/transforms.js +5 -0
  54. package/dist/shared/monitor.d.ts +144 -0
  55. package/dist/shared/monitor.js +12 -0
  56. package/dist/shared/parameters.d.ts +29 -0
  57. package/dist/shared/parameters.js +16 -0
  58. package/dist/shared/protocol.d.ts +156 -2
  59. package/dist/shared/protocol.js +2 -0
  60. package/dist/shared/rosbag.d.ts +104 -0
  61. package/dist/shared/rosbag.js +35 -0
  62. package/dist/types/client/api.d.ts +1 -1
  63. package/dist/types/client/connection.d.ts +60 -3
  64. package/dist/types/client/icons.d.ts +6 -0
  65. package/dist/types/client/sdk/index.d.ts +5 -2
  66. package/dist/types/shared/monitor.d.ts +144 -0
  67. package/dist/types/shared/parameters.d.ts +29 -0
  68. package/dist/types/shared/protocol.d.ts +156 -2
  69. package/dist/types/shared/rosbag.d.ts +104 -0
  70. package/dist/www/assets/Joystick.js +1 -1
  71. package/dist/www/assets/RobotModelView.js +2 -2
  72. package/dist/www/assets/TerminalView.js +2 -2
  73. package/dist/www/assets/dialogs.js +1 -1
  74. package/dist/www/assets/icons.js +1 -1
  75. package/dist/www/assets/index.css +1 -1
  76. package/dist/www/assets/index.js +47 -45
  77. package/dist/www/assets/shared/tassello-sdk.js +1 -1
  78. package/docs/plugins.md +15 -2
  79. 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 (ros2 bag record) when a page asks, until
13
- // one asks to stop: every page following it is told. A bag is played back for
14
- // the page that opens it (behavior_tree_playback.ts).
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
- // By topic
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?.recorder) {
310
+ if (!current) {
298
311
  return null;
299
312
  }
300
313
  this.recordings.delete(name);
301
314
  this.announce(name);
302
- await current.recorder.stop();
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, into ${current.path}`);
317
+ if (current !== undefined) {
318
+ throw new Error(`${name} is being recorded already${current ? `, into ${current}` : ''}`);
308
319
  }
309
- const recording = { path: '', since: 0, recorder: null };
310
- this.recordings.set(name, recording);
320
+ this.recordings.set(name, '');
321
+ let folder;
311
322
  try {
312
- recording.path = await newBagFolder(path);
313
- recording.recorder = await BagRecorder.start(recording.path, [name]);
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
- recording.since = Date.now();
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 recording.path;
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 recording = this.recordings.get(topic);
343
- return recording?.recorder ? { path: recording.path, since: recording.since } : null;
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() ?? []) {
@@ -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;
@@ -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, and for folders.
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;