tassello 0.1.1 → 0.3.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 (77) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +173 -6
  3. package/dist/server/behavior_tree_file.d.ts +17 -0
  4. package/dist/server/behavior_tree_file.js +232 -0
  5. package/dist/server/behavior_tree_files.d.ts +17 -0
  6. package/dist/server/behavior_tree_files.js +382 -0
  7. package/dist/server/behavior_tree_log.d.ts +10 -0
  8. package/dist/server/behavior_tree_log.js +132 -0
  9. package/dist/server/behavior_tree_playback.d.ts +57 -0
  10. package/dist/server/behavior_tree_playback.js +188 -0
  11. package/dist/server/behavior_tree_state.d.ts +38 -0
  12. package/dist/server/behavior_tree_state.js +220 -0
  13. package/dist/server/behavior_trees.d.ts +29 -0
  14. package/dist/server/behavior_trees.js +362 -0
  15. package/dist/server/diagnostics.d.ts +16 -0
  16. package/dist/server/diagnostics.js +200 -0
  17. package/dist/server/diagnostics_state.d.ts +23 -0
  18. package/dist/server/diagnostics_state.js +146 -0
  19. package/dist/server/disks.d.ts +14 -0
  20. package/dist/server/disks.js +152 -0
  21. package/dist/server/file_manager.d.ts +25 -0
  22. package/dist/server/file_manager.js +717 -0
  23. package/dist/server/files.d.ts +5 -2
  24. package/dist/server/files.js +32 -13
  25. package/dist/server/http.js +254 -9
  26. package/dist/server/netpbm.d.ts +3 -0
  27. package/dist/server/netpbm.js +69 -0
  28. package/dist/server/pty.d.ts +26 -0
  29. package/dist/server/pty.js +150 -0
  30. package/dist/server/rosbag.d.ts +25 -0
  31. package/dist/server/rosbag.js +418 -0
  32. package/dist/server/rosout.d.ts +16 -0
  33. package/dist/server/rosout.js +203 -0
  34. package/dist/server/rosout_log.d.ts +20 -0
  35. package/dist/server/rosout_log.js +66 -0
  36. package/dist/server/server.js +29 -6
  37. package/dist/server/session.d.ts +14 -1
  38. package/dist/server/session.js +130 -1
  39. package/dist/server/systemd.js +1 -1
  40. package/dist/server/systemd_format.d.ts +1 -0
  41. package/dist/server/systemd_format.js +19 -3
  42. package/dist/server/terminal_output.d.ts +25 -0
  43. package/dist/server/terminal_output.js +108 -0
  44. package/dist/server/terminals.d.ts +32 -0
  45. package/dist/server/terminals.js +366 -0
  46. package/dist/server/util.d.ts +4 -0
  47. package/dist/server/util.js +45 -1
  48. package/dist/shared/bt_nodes.d.ts +25 -0
  49. package/dist/shared/bt_nodes.js +180 -0
  50. package/dist/shared/file_manager.d.ts +130 -0
  51. package/dist/shared/file_manager.js +7 -0
  52. package/dist/shared/protocol.d.ts +242 -2
  53. package/dist/shared/protocol.js +12 -0
  54. package/dist/shared/xml.d.ts +25 -0
  55. package/dist/shared/xml.js +164 -0
  56. package/dist/types/client/api.d.ts +26 -3
  57. package/dist/types/client/connection.d.ts +52 -1
  58. package/dist/types/client/dialogs.d.ts +2 -1
  59. package/dist/types/client/icons.d.ts +51 -0
  60. package/dist/types/client/sdk/index.d.ts +5 -3
  61. package/dist/types/client/widgets/types.d.ts +5 -1
  62. package/dist/types/shared/bt_nodes.d.ts +25 -0
  63. package/dist/types/shared/file_manager.d.ts +130 -0
  64. package/dist/types/shared/protocol.d.ts +242 -2
  65. package/dist/www/assets/Joystick.js +1 -1
  66. package/dist/www/assets/RobotModelView.js +2 -2
  67. package/dist/www/assets/TerminalView.css +1 -0
  68. package/dist/www/assets/TerminalView.js +17 -0
  69. package/dist/www/assets/dialogs.js +1 -0
  70. package/dist/www/assets/icons.js +1 -1
  71. package/dist/www/assets/index.css +1 -1
  72. package/dist/www/assets/index.js +69 -2
  73. package/dist/www/assets/keys.js +3 -0
  74. package/dist/www/assets/shared/tassello-sdk.js +1 -1
  75. package/dist/www/index.html +2 -0
  76. package/docs/plugins.md +28 -0
  77. package/package.json +6 -1
@@ -0,0 +1,382 @@
1
+ // The behavior trees' XML files, for the behavior tree editors: read for
2
+ // their pages, and again whenever they change (saved by a page, or edited on
3
+ // the robot), with the nodes they can use: Nav2's, those of their includes
4
+ // and of the libraries the pages name (TreeNodesModel). And saved, only over
5
+ // behavior trees, and only if unchanged since the page read them.
6
+ import { promises as fs } from 'node:fs';
7
+ import path from 'node:path';
8
+ import { readModels } from '../shared/bt_nodes.js';
9
+ import { parseXmlSource, sourceText } from '../shared/xml.js';
10
+ import { resolveTreeFile } from './behavior_tree_file.js';
11
+ import { isBehaviorTreeXml } from './files.js';
12
+ import { contentVersion, errorMessage, packageShareDirectory, replaceFile } from './util.js';
13
+ // How often the files are checked for changes
14
+ const POLL_MS = 2000;
15
+ const MAX_FILE_BYTES = 5 * 1024 * 1024;
16
+ // Includes of includes, at most
17
+ const MAX_INCLUDES = 50;
18
+ // A file of models kept parsed while unchanged; this many
19
+ const MODEL_CACHE = 64;
20
+ // Size, time and inode of a file, '' if gone
21
+ async function stamp(file) {
22
+ const stat = await fs.stat(file).catch(() => null);
23
+ return stat ? `${stat.size}-${stat.mtimeMs}-${stat.ino}` : '';
24
+ }
25
+ async function readText(file) {
26
+ const stat = await fs.stat(file);
27
+ if (!stat.isFile()) {
28
+ throw new Error(`${file} is not a file`);
29
+ }
30
+ if (stat.size > MAX_FILE_BYTES) {
31
+ throw new Error(`${file} is larger than ${MAX_FILE_BYTES / 1024 / 1024} MB`);
32
+ }
33
+ return fs.readFile(file, 'utf8');
34
+ }
35
+ function modelElement(text, element) {
36
+ return {
37
+ name: element.name,
38
+ attributes: element.attributes,
39
+ children: element.children.flatMap((child) => (child.type === 'element' ? [modelElement(text, child)] : [])),
40
+ text: sourceText(text, element),
41
+ };
42
+ }
43
+ // The includes of a tree's root, resolved: relative to its file, or to their
44
+ // ros_pkg's share directory
45
+ function includes(root, file, warnings) {
46
+ return root.children.flatMap((child) => {
47
+ if (child.type !== 'element' || child.name !== 'include') {
48
+ return [];
49
+ }
50
+ const { ros_pkg: rosPackage, path: relative = '' } = child.attributes;
51
+ const share = rosPackage ? packageShareDirectory(rosPackage) : path.dirname(file);
52
+ if (!share) {
53
+ warnings.push(`Not included: ${relative}, of the package ${rosPackage}, not found`);
54
+ return [];
55
+ }
56
+ return [path.resolve(share, relative)];
57
+ });
58
+ }
59
+ const modelCache = new Map();
60
+ // A file's models and BehaviorTrees, parsed again only when it changes
61
+ async function readModelFile(file) {
62
+ const current = await stamp(file);
63
+ const cached = modelCache.get(file);
64
+ if (cached && cached.stamp === current) {
65
+ return cached.parsed;
66
+ }
67
+ const text = await readText(file).catch((e) => {
68
+ throw new Error(`Cannot read ${file}: ${e.code ?? errorMessage(e)}`);
69
+ });
70
+ const root = parseXmlSource(text);
71
+ if (root.name !== 'root') {
72
+ throw new Error(`${file} is not of BT.CPP: its root element is <${root.name}>, not <root>`);
73
+ }
74
+ const parsed = {
75
+ models: readModels(modelElement(text, root)),
76
+ trees: root.children.flatMap((child) => (child.type === 'element' && child.name === 'BehaviorTree' && child.attributes.ID ? [child.attributes.ID] : [])),
77
+ root,
78
+ };
79
+ modelCache.delete(file);
80
+ modelCache.set(file, { stamp: current, parsed });
81
+ if (modelCache.size > MODEL_CACHE) {
82
+ modelCache.delete(modelCache.keys().next().value);
83
+ }
84
+ return parsed;
85
+ }
86
+ // The models of Nav2's nodes, if installed
87
+ function nav2Models() {
88
+ const share = packageShareDirectory('nav2_behavior_tree');
89
+ return share && path.join(share, 'nav2_tree_nodes.xml');
90
+ }
91
+ // What a page edits: the file, read again when it changes, for all its pages
92
+ class WatchedFile {
93
+ file;
94
+ readOnly;
95
+ watchers = new Set();
96
+ state = null;
97
+ stamp = null;
98
+ timer = null;
99
+ checking = null;
100
+ constructor(file, readOnly) {
101
+ this.file = file;
102
+ this.readOnly = readOnly;
103
+ }
104
+ get text() {
105
+ return this.state?.text ?? null;
106
+ }
107
+ add(watcher) {
108
+ this.watchers.add(watcher);
109
+ if (this.state) {
110
+ watcher.onFile(this.state);
111
+ }
112
+ this.timer ??= setInterval(() => void this.check(), POLL_MS);
113
+ void this.check();
114
+ }
115
+ // false once it has no watcher left
116
+ remove(watcher) {
117
+ this.watchers.delete(watcher);
118
+ if (this.watchers.size > 0) {
119
+ return true;
120
+ }
121
+ this.stop();
122
+ return false;
123
+ }
124
+ stop() {
125
+ if (this.timer) {
126
+ clearInterval(this.timer);
127
+ this.timer = null;
128
+ }
129
+ }
130
+ // Reads it again if it changed: once at a time
131
+ check() {
132
+ this.checking ??= this.read().finally(() => (this.checking = null));
133
+ return this.checking;
134
+ }
135
+ // After a check started before it changed
136
+ async refresh() {
137
+ await this.checking;
138
+ await this.check();
139
+ }
140
+ async read() {
141
+ const current = await stamp(this.file);
142
+ if (current === this.stamp) {
143
+ return;
144
+ }
145
+ this.stamp = current;
146
+ let state;
147
+ try {
148
+ const text = await readText(this.file);
149
+ let readOnly = this.readOnly;
150
+ if (!readOnly) {
151
+ readOnly = await fs.access(await fs.realpath(this.file), fs.constants.W_OK).then(() => null, () => 'The server’s user cannot write it: save it elsewhere');
152
+ }
153
+ state = { file: this.file, text, version: contentVersion(text), readOnly, error: null };
154
+ }
155
+ catch (e) {
156
+ const code = e.code;
157
+ const error = code === 'ENOENT' ? `No file ${this.file}` : `Cannot read ${this.file}: ${code ?? errorMessage(e)}`;
158
+ state = { file: this.file, text: null, version: null, readOnly: this.readOnly, error };
159
+ }
160
+ if (state.version !== this.state?.version || state.error !== this.state?.error || state.readOnly !== this.state?.readOnly) {
161
+ this.state = state;
162
+ for (const watcher of this.watchers) {
163
+ watcher.onFile(state);
164
+ }
165
+ }
166
+ }
167
+ }
168
+ // The models a page can use with its file and libraries: again whenever the
169
+ // file's includes, or any file read for them, change
170
+ class ModelsWatch {
171
+ watched;
172
+ libraries;
173
+ uploadsDir;
174
+ onModels;
175
+ // The files read, and their stamps then
176
+ stamps = new Map();
177
+ text = undefined;
178
+ sent = '';
179
+ timer;
180
+ running = false;
181
+ // Asked again while checking
182
+ again = false;
183
+ closed = false;
184
+ constructor(watched, libraries, uploadsDir, onModels) {
185
+ this.watched = watched;
186
+ this.libraries = libraries;
187
+ this.uploadsDir = uploadsDir;
188
+ this.onModels = onModels;
189
+ this.timer = setInterval(() => void this.check(), POLL_MS);
190
+ }
191
+ close() {
192
+ this.closed = true;
193
+ clearInterval(this.timer);
194
+ }
195
+ async check() {
196
+ if (this.running) {
197
+ this.again = true;
198
+ return;
199
+ }
200
+ this.running = true;
201
+ try {
202
+ do {
203
+ this.again = false;
204
+ let changed = this.watched.text !== this.text;
205
+ for (const [file, known] of this.stamps) {
206
+ changed ||= (await stamp(file)) !== known;
207
+ }
208
+ if (changed && !this.closed) {
209
+ await this.read();
210
+ }
211
+ } while (this.again && !this.closed);
212
+ }
213
+ finally {
214
+ this.running = false;
215
+ }
216
+ }
217
+ async read() {
218
+ this.text = this.watched.text;
219
+ const stamps = new Map();
220
+ const libraries = [];
221
+ // The first of each ID
222
+ const trees = new Map();
223
+ const warnings = [];
224
+ const load = async (file) => {
225
+ stamps.set(file, await stamp(file));
226
+ return readModelFile(file);
227
+ };
228
+ const nav2 = nav2Models();
229
+ if (nav2) {
230
+ try {
231
+ libraries.push({ name: 'Nav2', file: nav2, models: (await load(nav2)).models });
232
+ }
233
+ catch {
234
+ // Not readable: Nav2's nodes are known by name anyway
235
+ }
236
+ }
237
+ if (this.text !== null) {
238
+ // The includes, and theirs
239
+ const queue = [];
240
+ try {
241
+ queue.push(...includes(parseXmlSource(this.text), this.watched.file, warnings));
242
+ }
243
+ catch {
244
+ // Not XML, as the page will say
245
+ }
246
+ const seen = new Set([this.watched.file]);
247
+ while (queue.length > 0 && seen.size <= MAX_INCLUDES) {
248
+ const file = queue.shift();
249
+ if (seen.has(file)) {
250
+ continue;
251
+ }
252
+ seen.add(file);
253
+ try {
254
+ const included = await load(file);
255
+ for (const id of included.trees) {
256
+ if (!trees.has(id)) {
257
+ trees.set(id, file);
258
+ }
259
+ }
260
+ if (included.models.length > 0) {
261
+ libraries.push({ name: path.basename(file), file, models: included.models });
262
+ }
263
+ queue.push(...includes(included.root, file, warnings));
264
+ }
265
+ catch (e) {
266
+ warnings.push(`Not included: ${errorMessage(e)}`);
267
+ }
268
+ }
269
+ }
270
+ for (const source of this.libraries) {
271
+ try {
272
+ const file = resolveTreeFile(source, this.uploadsDir);
273
+ const library = await load(file);
274
+ if (library.models.length === 0) {
275
+ warnings.push(`No node models (TreeNodesModel) in the library ${source}`);
276
+ }
277
+ libraries.push({ name: source.startsWith('uploads/') ? source : path.basename(file), file, models: library.models });
278
+ }
279
+ catch (e) {
280
+ warnings.push(`Library ${source}: ${errorMessage(e)}`);
281
+ }
282
+ }
283
+ if (this.closed) {
284
+ return;
285
+ }
286
+ this.stamps = stamps;
287
+ const models = { libraries, trees: [...trees].map(([id, file]) => ({ id, file })), warnings };
288
+ const text = JSON.stringify(models);
289
+ if (text !== this.sent) {
290
+ this.sent = text;
291
+ this.onModels(models);
292
+ }
293
+ }
294
+ }
295
+ export class BehaviorTreeFiles {
296
+ uploadsDir;
297
+ logger;
298
+ // By file
299
+ watched = new Map();
300
+ constructor(uploadsDir, logger) {
301
+ this.uploadsDir = uploadsDir;
302
+ this.logger = logger;
303
+ }
304
+ // The file the page names, and the models it can use: returns what ends it
305
+ watch(source, libraries, watcher) {
306
+ let file;
307
+ try {
308
+ file = resolveTreeFile(source, this.uploadsDir);
309
+ }
310
+ catch (e) {
311
+ watcher.onFile({ file: '', text: null, version: null, readOnly: null, error: errorMessage(e) });
312
+ watcher.onModels({ libraries: [], trees: [], warnings: [] });
313
+ return () => { };
314
+ }
315
+ let watched = this.watched.get(file);
316
+ if (!watched) {
317
+ watched = new WatchedFile(file, source.trim().startsWith('uploads/') ? 'An upload: save it on the robot' : null);
318
+ this.watched.set(file, watched);
319
+ }
320
+ const shared = watched;
321
+ const models = new ModelsWatch(shared, libraries, this.uploadsDir, watcher.onModels);
322
+ // Its includes may have changed with it
323
+ const own = {
324
+ onFile: (state) => {
325
+ watcher.onFile(state);
326
+ void models.check();
327
+ },
328
+ onModels: watcher.onModels,
329
+ };
330
+ shared.add(own);
331
+ void models.check();
332
+ return () => {
333
+ models.close();
334
+ if (!shared.remove(own)) {
335
+ this.watched.delete(file);
336
+ }
337
+ };
338
+ }
339
+ // Saves the file: over a behavior tree, unless it changed since base (null:
340
+ // whatever it is), or a new one (create)
341
+ async save(source, text, base, create) {
342
+ if (source.trim().startsWith('uploads/')) {
343
+ throw new Error('Uploads are not saved: save it on the robot');
344
+ }
345
+ const file = resolveTreeFile(source, this.uploadsDir);
346
+ if (Buffer.byteLength(text) > MAX_FILE_BYTES) {
347
+ throw new Error(`Larger than ${MAX_FILE_BYTES / 1024 / 1024} MB`);
348
+ }
349
+ const root = parseXmlSource(text);
350
+ if (root.name !== 'root') {
351
+ throw new Error(`Not a behavior tree: its root element is <${root.name}>, not <root>`);
352
+ }
353
+ if (create) {
354
+ await fs.mkdir(path.dirname(file), { recursive: true });
355
+ await fs.writeFile(file, text, { flag: 'wx' }).catch((e) => {
356
+ throw new Error(e.code === 'EEXIST' ? `${file} exists already` : errorMessage(e));
357
+ });
358
+ }
359
+ else {
360
+ const real = await fs.realpath(file).catch(() => {
361
+ throw new Error(`No file ${file}`);
362
+ });
363
+ const current = await readText(real);
364
+ if (!isBehaviorTreeXml(current)) {
365
+ throw new Error(`${file} is not a behavior tree (its root element is not <root>): not overwritten`);
366
+ }
367
+ if (base !== null && contentVersion(current) !== base) {
368
+ return { version: contentVersion(current), conflict: true };
369
+ }
370
+ await replaceFile(real, text);
371
+ }
372
+ this.logger.info(`Behavior tree saved: ${file}`);
373
+ await this.watched.get(file)?.refresh();
374
+ return { version: contentVersion(text), conflict: false };
375
+ }
376
+ stopAll() {
377
+ for (const watched of this.watched.values()) {
378
+ watched.stop();
379
+ }
380
+ this.watched.clear();
381
+ }
382
+ }
@@ -0,0 +1,10 @@
1
+ export interface LogTransition {
2
+ uid: number | null;
3
+ name: string;
4
+ from: string;
5
+ to: string;
6
+ time: number;
7
+ }
8
+ export declare function logTransitions(raw: unknown): LogTransition[];
9
+ export declare function decodeLog(data: Uint8Array, uids: boolean): LogTransition[];
10
+ export declare function logLayout(messages: Iterable<Uint8Array>): boolean | null;
@@ -0,0 +1,132 @@
1
+ // The messages of a behavior tree's log (nav2_msgs/msg/BehaviorTreeLog): as
2
+ // rclnodejs gives them, live, and as rosbags keep them, CDR serialized. Their
3
+ // transitions in either case.
4
+ //
5
+ // builtin_interfaces/Time timestamp
6
+ // BehaviorTreeStatusChange[] event_log
7
+ // builtin_interfaces/Time timestamp
8
+ // string node_name
9
+ // uint16 uid (since Jazzy)
10
+ // string previous_status
11
+ // string current_status
12
+ const STATUSES = new Set(['IDLE', 'RUNNING', 'SUCCESS', 'FAILURE', 'SKIPPED']);
13
+ const toMs = (time) => (time?.sec ? time.sec * 1000 + (time.nanosec ?? 0) / 1e6 : 0);
14
+ // A message's transitions, as rclnodejs gives it; throws if it is not a log
15
+ export function logTransitions(raw) {
16
+ const log = raw;
17
+ if (!Array.isArray(log?.event_log)) {
18
+ throw new Error('not a log of transitions (nav2_msgs/msg/BehaviorTreeLog)');
19
+ }
20
+ const sent = toMs(log.timestamp) || Date.now();
21
+ return log.event_log.map((event) => ({
22
+ uid: typeof event.uid === 'number' && event.uid > 0 ? event.uid : null,
23
+ name: String(event.node_name ?? ''),
24
+ from: String(event.previous_status ?? '').toUpperCase(),
25
+ to: String(event.current_status ?? '').toUpperCase(),
26
+ time: toMs(event.timestamp) || sent,
27
+ }));
28
+ }
29
+ const utf8 = new TextDecoder();
30
+ // Reads CDR (XCDR 1 or 2, of final types: the same here) after its 4 byte
31
+ // encapsulation header, which says its endianness; aligned from after it
32
+ class CdrReader {
33
+ offset = 4;
34
+ view;
35
+ little;
36
+ constructor(data) {
37
+ if (data.length < 4) {
38
+ throw new Error('Truncated message');
39
+ }
40
+ this.view = new DataView(data.buffer, data.byteOffset, data.byteLength);
41
+ this.little = (data[1] & 1) === 1;
42
+ }
43
+ take(size) {
44
+ this.offset += (size - ((this.offset - 4) % size)) % size;
45
+ const at = this.offset;
46
+ if (at + size > this.view.byteLength) {
47
+ throw new Error('Truncated message');
48
+ }
49
+ this.offset += size;
50
+ return at;
51
+ }
52
+ int32() {
53
+ return this.view.getInt32(this.take(4), this.little);
54
+ }
55
+ uint32() {
56
+ return this.view.getUint32(this.take(4), this.little);
57
+ }
58
+ uint16() {
59
+ return this.view.getUint16(this.take(2), this.little);
60
+ }
61
+ // Its length counts the terminating null
62
+ string() {
63
+ const length = this.uint32();
64
+ if (length === 0) {
65
+ return '';
66
+ }
67
+ if (this.offset + length > this.view.byteLength) {
68
+ throw new Error('Truncated message');
69
+ }
70
+ const text = utf8.decode(new Uint8Array(this.view.buffer, this.view.byteOffset + this.offset, length - 1));
71
+ this.offset += length;
72
+ return text;
73
+ }
74
+ time() {
75
+ const sec = this.int32();
76
+ return sec * 1000 + this.uint32() / 1e6;
77
+ }
78
+ }
79
+ // A serialized log's transitions, with UIDs (Jazzy's) or without; throws if
80
+ // it is not a log of that layout
81
+ export function decodeLog(data, uids) {
82
+ const reader = new CdrReader(data);
83
+ const sent = reader.time();
84
+ const count = reader.uint32();
85
+ const transitions = [];
86
+ for (let i = 0; i < count; i++) {
87
+ const time = reader.time();
88
+ const name = reader.string();
89
+ const uid = uids ? reader.uint16() : 0;
90
+ const from = reader.string();
91
+ const to = reader.string();
92
+ if (!STATUSES.has(from) || !STATUSES.has(to)) {
93
+ throw new Error(`not a log of transitions: statuses “${from}”, “${to}”`);
94
+ }
95
+ transitions.push({ uid: uid > 0 ? uid : null, name, from, to, time: time || sent });
96
+ }
97
+ return transitions;
98
+ }
99
+ // How many messages tell the layout
100
+ const LAYOUT_SAMPLES = 200;
101
+ // Whether the serialized logs have UIDs, from their first transitions; null
102
+ // if neither layout reads them. Either may read the other's, the UIDs in the
103
+ // strings' padding: with UIDs, those of a log without them read 0, which
104
+ // BT.CPP never numbers.
105
+ export function logLayout(messages) {
106
+ const samples = [];
107
+ for (const data of messages) {
108
+ if (samples.length === LAYOUT_SAMPLES) {
109
+ break;
110
+ }
111
+ // Not empty: an empty log reads either way
112
+ if (data.length > 16) {
113
+ samples.push(data);
114
+ }
115
+ }
116
+ const read = (uids) => {
117
+ const transitions = [];
118
+ try {
119
+ for (const data of samples) {
120
+ transitions.push(...decodeLog(data, uids));
121
+ }
122
+ }
123
+ catch {
124
+ return null;
125
+ }
126
+ return transitions;
127
+ };
128
+ if (read(true)?.some((transition) => transition.uid !== null)) {
129
+ return true;
130
+ }
131
+ return read(false)?.length ? false : null;
132
+ }
@@ -0,0 +1,57 @@
1
+ import { type BehaviorTreePlaybackState } from '../shared/protocol.ts';
2
+ import { type LogTransition } from './behavior_tree_log.ts';
3
+ import type { BehaviorTreeWatcher } from './behavior_trees.ts';
4
+ import { type Logger } from './util.ts';
5
+ export type PlaybackState = Omit<BehaviorTreePlaybackState, 'type' | 'id'>;
6
+ export type PlaybackWatcher = Omit<BehaviorTreeWatcher, 'onRecording'> & {
7
+ onPlayback: (state: PlaybackState) => void;
8
+ };
9
+ export interface PlaybackOptions {
10
+ bag: string;
11
+ topic: string;
12
+ tree: string;
13
+ uploadsDir: string;
14
+ logger: Logger;
15
+ }
16
+ interface Timed {
17
+ time: number;
18
+ transitions: LogTransition[];
19
+ }
20
+ export declare function readLog(bag: string, topic: string): Promise<{
21
+ topic: string;
22
+ messages: Timed[];
23
+ }>;
24
+ export declare class BehaviorTreePlayback {
25
+ private readonly watcher;
26
+ private nodes;
27
+ private messages;
28
+ private state;
29
+ private next;
30
+ private topic;
31
+ private start;
32
+ private end;
33
+ private time;
34
+ private playing;
35
+ private speed;
36
+ private error;
37
+ private timer;
38
+ private lastTick;
39
+ private lastFlush;
40
+ private lastState;
41
+ private closed;
42
+ constructor(options: PlaybackOptions, watcher: PlaybackWatcher);
43
+ control({ playing, speed, time }: {
44
+ playing?: boolean;
45
+ speed?: number;
46
+ time?: number;
47
+ }): void;
48
+ close(): void;
49
+ private open;
50
+ private tick;
51
+ private advance;
52
+ private seek;
53
+ private flush;
54
+ private sendState;
55
+ private stopTimer;
56
+ }
57
+ export {};