tassello 0.1.1 → 0.2.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 (51) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +66 -3
  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 +18 -0
  6. package/dist/server/behavior_tree_files.js +405 -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/files.d.ts +5 -2
  16. package/dist/server/files.js +32 -13
  17. package/dist/server/http.js +7 -5
  18. package/dist/server/rosbag.d.ts +25 -0
  19. package/dist/server/rosbag.js +418 -0
  20. package/dist/server/rosout.d.ts +16 -0
  21. package/dist/server/rosout.js +203 -0
  22. package/dist/server/rosout_log.d.ts +20 -0
  23. package/dist/server/rosout_log.js +66 -0
  24. package/dist/server/server.js +20 -4
  25. package/dist/server/session.d.ts +7 -0
  26. package/dist/server/session.js +63 -0
  27. package/dist/server/systemd.js +1 -1
  28. package/dist/server/systemd_format.d.ts +1 -0
  29. package/dist/server/systemd_format.js +19 -3
  30. package/dist/shared/bt_nodes.d.ts +25 -0
  31. package/dist/shared/bt_nodes.js +180 -0
  32. package/dist/shared/protocol.d.ts +157 -2
  33. package/dist/shared/protocol.js +4 -0
  34. package/dist/shared/xml.d.ts +25 -0
  35. package/dist/shared/xml.js +164 -0
  36. package/dist/types/client/api.d.ts +4 -3
  37. package/dist/types/client/connection.d.ts +31 -1
  38. package/dist/types/client/dialogs.d.ts +2 -1
  39. package/dist/types/client/icons.d.ts +24 -0
  40. package/dist/types/client/sdk/index.d.ts +3 -2
  41. package/dist/types/client/widgets/types.d.ts +5 -1
  42. package/dist/types/shared/bt_nodes.d.ts +25 -0
  43. package/dist/types/shared/protocol.d.ts +157 -2
  44. package/dist/www/assets/Joystick.js +1 -1
  45. package/dist/www/assets/RobotModelView.js +2 -2
  46. package/dist/www/assets/icons.js +1 -1
  47. package/dist/www/assets/index.css +1 -1
  48. package/dist/www/assets/index.js +28 -1
  49. package/dist/www/assets/shared/tassello-sdk.js +1 -1
  50. package/docs/plugins.md +8 -0
  51. package/package.json +2 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,32 @@
3
3
  Tassello follows [semantic versioning](https://semver.org): before 1.0, a new minor version may
4
4
  change the plugin API (`tassello/sdk`, `tassello/server`, `tassello/vite`), as noted here.
5
5
 
6
+ ## Unreleased
7
+
8
+ - Behavior Tree Inspector widget: what a BT.CPP behavior tree is doing, from its
9
+ `nav2_msgs/BehaviorTreeLog` (Nav2's `/behavior_tree_log`), on the tree of its XML file: the
10
+ running branch, each node's status, last result, counts and ports, and the latest transitions,
11
+ as a graph or a list. The server follows the log for every page and keeps the nodes' state.
12
+ - The inspector records its log into rosbags (`ros2 bag record`), and plays rosbags back in the
13
+ widget (MCAP or SQLite, Humble's or Jazzy's), with pause, seek and speed.
14
+ - Behavior Tree Editor widget: a BT.CPP behavior tree's XML file of the robot, edited as a graph,
15
+ with a palette of nodes (BT.CPP's, Nav2's, node libraries of `TreeNodesModel`, the file's own),
16
+ drag and drop, a side panel for the nodes' names and ports, subtrees, undo and redo, copy and
17
+ paste, and an XML view. Saves only what changed, keeping the file's comments and layout, and
18
+ never over changes made on the robot since it was read.
19
+ - The file browser is resizable, and shows long names whole.
20
+ - Plugins: the `behavior-tree` property kind, `connection.subscribeBehaviorTree`,
21
+ `recordBehaviorTree`, `subscribeBehaviorTreePlayback`, `controlBehaviorTreePlayback`,
22
+ `subscribeBehaviorTreeFile`, `saveBehaviorTreeFile`.
23
+ - Uploads take behavior trees (XML) as well as images.
24
+ - Logs widget: the ROS log (`/rosout`), as rqt_console and swri_console show it: every node's
25
+ messages coloured by level, the nodes running and those that logged with their counts, filters
26
+ by node, level and text (terms or regular expressions, to show or to hide), pause, follow,
27
+ clear, download, and a message's details. A widget can be for some nodes only (`amcl,
28
+ *_server`). The server follows the log for every page and keeps its latest 10,000 messages.
29
+ - Plugins: `connection.subscribeRosout`, and the `LogsIcon`, `NodesIcon`, `SearchIcon`,
30
+ `BanIcon`, `FilterIcon`, `DownloadIcon` and `ArrowDownToLineIcon` icons.
31
+
6
32
  ## 0.1.0
7
33
 
8
34
  The first release, from the Dieletrikbot's web dashboard.
package/README.md CHANGED
@@ -10,7 +10,8 @@ and plugins add more.
10
10
  - **Panels of widgets** on a grid: add, move and resize them in the browser, no code or config files.
11
11
  - **Every device**: a desktop canvas, and layouts of their own for tablets and phones.
12
12
  - **Built in**: teleop pads, a Nav2 map with goals, the robot's URDF in 3D, an arm jogger, cameras,
13
- images, command buttons and a systemd application manager.
13
+ images, command buttons, a systemd application manager, a behavior tree inspector and
14
+ editor, and the ROS log of every node.
14
15
  - **Plugins**: widgets of your own, with server-side code if they need it, as npm or ROS packages.
15
16
  - **One process**: a ROS 2 node in Node.js. Nothing to install on the robot but Node.js.
16
17
 
@@ -113,6 +114,9 @@ after the others on the devices already arranged.
113
114
  | **Image Viewer** | A static image: a file of the robot, typed (`/…`, `~/…`, `package://…`) or picked by browsing its folders, or an image uploaded from the page's device |
114
115
  | **Button** | Runs a bash command on the robot, showing its output. Takes its running colour until the command finishes, on every page showing it; pressing it again stops the command |
115
116
  | **Application Manager** | The robot's systemd services matching a name: their state, whether they start at boot, buttons to start, stop and restart them, and their journal |
117
+ | **Behavior Tree Inspector** | What a BT.CPP behavior tree is doing, from its log (Nav2's `/behavior_tree_log`): the tree of its XML file with the running branch lit, each node's status, last result, counts and ports, and the latest transitions. As a graph (pan, zoom, follow the running nodes) or a list. Records the log into rosbags, and plays rosbags back at any speed |
118
+ | **Behavior Tree Editor** | Edits a BT.CPP behavior tree's XML file of the robot as a graph: nodes from a palette (BT.CPP's, Nav2's, your libraries'), dragged where they go, moved with their children, their names and ports edited in a side panel. Subtrees, undo and redo, copy and paste, an XML view. Saves only what changed, never over changes made meanwhile |
119
+ | **Logs** | The ROS log (`/rosout`), as rqt_console and swri_console show it: every node's messages coloured by level, the nodes running and those that logged with their counts, filtered by node, by level and by text (terms or a regular expression, to show or to hide). Pause, follow the newest, clear, download, and a message's details: where it was logged |
116
120
 
117
121
  Their properties (topics, frames, speeds, colours, ...) are edited in the side panel of edit mode.
118
122
 
@@ -153,6 +157,58 @@ Notes on some of them:
153
157
  - **Image Viewer** reads only images (PNG, JPEG, GIF, WebP, BMP, SVG). Uploads are kept next to
154
158
  the panels (`~/.local/share/tassello/images`). Images are served sandboxed: an SVG cannot run
155
159
  scripts in the dashboard.
160
+ - **Behavior Tree Inspector** follows a `nav2_msgs/BehaviorTreeLog` topic, which BT.CPP trees
161
+ log with Nav2's `RosTopicLogger`: Nav2's bt_navigator on `/behavior_tree_log`. Its *tree file*
162
+ is the XML the robot runs (`/…`, `~/…`, `package://…`, browsed or uploaded), read with its
163
+ includes and subtrees, and again when it changes; e.g. Nav2's default,
164
+ `package://nav2_bt_navigator/behavior_trees/navigate_to_pose_w_replanning_and_recovery.xml`.
165
+ Without it, the nodes are listed as the log names them. Since Jazzy, the log gives the nodes'
166
+ UIDs, which tell them apart exactly; before, nodes of the same name (a `RecoveryNode` named
167
+ after its action) are told apart by their statuses and their parents'. The log only says what
168
+ changes: the server follows it for every page, and for 10 minutes after the last one closes,
169
+ so that a page opened later sees the nodes as they are. A node reset to idle by its parent
170
+ shows how it last ended.
171
+
172
+ Its record button records the log on the robot with `ros2 bag record`, into a new folder
173
+ (`<recordings folder>/behavior_tree_<date>_<time>` by default), until pressed again: every
174
+ page showing the topic shows it recording. Its playback button opens a rosbag of the robot
175
+ (a bag's folder, or its `.mcap` or `.db3` file, e.g. of a whole run recorded with
176
+ `ros2 bag record -a`) and plays its log in the widget, on the tree of its file, without
177
+ publishing anything: pause, seek and choose the speed (0.1× to 16×). Bags of Humble and of
178
+ Jazzy are read either way; SQLite bags need Node.js 22.13, zstd compressed MCAP 22.15.
179
+ - **Behavior Tree Editor** edits one BehaviorTree of the file at a time (its toolbar switches
180
+ between them, adds new ones, and double clicking a subtree opens its tree, even in an included
181
+ file). Add a node from the palette by dragging it where it goes (into a node, or beside one), or
182
+ click it to add it into the selected node, or after it. Drag nodes to move them with their
183
+ children. The side panel edits the selected node's name and ports, with their types, defaults
184
+ and descriptions from its model, and its other attributes (BT.CPP 4's `_skipIf`, `_while`…);
185
+ it moves, duplicates, wraps, unwraps and replaces it. What BT.CPP would refuse, such as a
186
+ decorator without its child, is marked. Keys: Delete, Ctrl+Z, Ctrl+Shift+Z, Ctrl+C, Ctrl+X,
187
+ Ctrl+V, Ctrl+D, Ctrl+S, the arrows to move the selection (Alt: the node).
188
+
189
+ The palette has BT.CPP's nodes (of the file's format, `BTCPP_format`), Nav2's (its
190
+ `nav2_tree_nodes.xml`, when installed), those of the file's `TreeNodesModel` and includes, and
191
+ those of the *node libraries*: XML files of `TreeNodesModel`, as Groot exports them, set in the
192
+ widget's properties or loaded from the palette. Saving writes back only the elements changed,
193
+ the rest of the file (comments, blank lines, indentation) as it was. The file is read again when
194
+ it changes on the robot: without changes in the editor it is shown at once, with changes the
195
+ editor asks which to keep, and a save over a file changed since it was read is refused.
196
+ Uploads, and files the server cannot write, are saved elsewhere with *Save as*. The editor keeps
197
+ its changes while the page is open, and the page asks before closing with changes not saved.
198
+ - **Logs** follows `/rosout` (`rcl_interfaces/msg/Log`), on which every node's logger publishes.
199
+ Its list of nodes has those running in the ROS graph and those that logged, with their
200
+ warnings and errors: choose some to see only theirs. A node's logger is named after it, dots
201
+ for slashes (`robot1.amcl` for `/robot1/amcl`). The level buttons show and hide each level
202
+ (Shift+click: that level only); *Include* and *Exclude* keep or drop the messages containing any
203
+ of their terms, separated by `;`, or matching a regular expression. Pausing holds the list while
204
+ the new messages wait; clearing empties it for this page. A message clicked shows whole, with its
205
+ file, line and function; the arrow keys go through the list. *Download* saves the messages shown
206
+ as ROS prints them (`[WARN] [1729435407.512346] [robot1.amcl]: ...`). The widget's properties
207
+ set its nodes (names or globs, e.g. `amcl, *_server`: one widget per subsystem), the levels and
208
+ filters it opens with, and the time shown: the clock, ROS stamps, or none. The server follows the
209
+ log for every page, keeping its latest 10,000 messages, and for 10 minutes after the last page
210
+ closes: a page opened later sees what was logged meanwhile, and when first opened, what each
211
+ node logged in its last 10 seconds (rosout's own history).
156
212
 
157
213
  ## Plugins
158
214
 
@@ -180,8 +236,10 @@ and ship example panels. See **[docs/plugins.md](docs/plugins.md)**.
180
236
  ## Security
181
237
 
182
238
  Tassello has no login. **Anyone who can open the page can drive the robot, run any command as
183
- the server's user and read its images**: keep it on a network you trust, or behind a reverse
184
- proxy with authentication (it serves everything under `/`, its WebSocket at `/ws`).
239
+ the server's user, read its images, behavior trees and the behavior tree logs of its rosbags,
240
+ record rosbags into new folders, and write behavior tree files (XML files whose root is
241
+ `<root>`, over others of the same kind only)**: keep it on a network you trust, or behind a reverse proxy
242
+ with authentication (it serves everything under `/`, its WebSocket at `/ws`).
185
243
 
186
244
  ## How it works
187
245
 
@@ -207,6 +265,11 @@ Safety and bandwidth are the server's concern:
207
265
  of their publishers, so latched maps arrive.
208
266
  - Topics are subscribed once, whatever the number of pages, with the type and QoS of their
209
267
  publishers; TF only while a page shows the robot.
268
+ - Behavior tree logs, streams of transitions, are followed whole, none dropped: each page gets
269
+ the nodes that changed, at most 10 times a second, however fast the tree ticks.
270
+ - The ROS log is followed once for every page: each gets the new messages at most 10 times a
271
+ second, at most 500 at a time (a node logging faster has some skipped, which the page shows).
272
+ Only the rows in sight are drawn.
210
273
  - A button runs the command saved in its panel, never one sent by the page.
211
274
 
212
275
  ## Developing
@@ -0,0 +1,17 @@
1
+ import type { BehaviorTreeInfo, BtNode, BtNodeKind } from '../shared/protocol.ts';
2
+ import { type XmlElement } from '../shared/xml.ts';
3
+ import { type Logger } from './util.ts';
4
+ export interface BehaviorTreeFile {
5
+ treeId: string;
6
+ nodes: BtNode[];
7
+ warnings: string[];
8
+ files: string[];
9
+ }
10
+ export declare function resolveTreeFile(source: string, uploadsDir: string, home?: string): string;
11
+ export declare function instantiate(trees: Map<string, XmlElement>, mainId: string, kinds: Map<string, BtNodeKind>, warnings: string[]): BtNode[];
12
+ export declare function loadBehaviorTree(file: string): Promise<BehaviorTreeFile>;
13
+ export type TreeInfo = Omit<BehaviorTreeInfo, 'type' | 'id'>;
14
+ export declare function readTree(source: string, uploadsDir: string, logger: Logger): Promise<{
15
+ tree: TreeInfo;
16
+ files: string[];
17
+ }>;
@@ -0,0 +1,232 @@
1
+ // The behavior trees' XML files (BT.CPP's format, 3 and 4), for the behavior
2
+ // tree inspector widgets: the tree the robot runs, as BT.CPP instantiates it,
3
+ // its nodes in the order of their UIDs: depth first, from the first child of
4
+ // the main BehaviorTree, each SubTree followed by its tree's nodes.
5
+ import { promises as fs } from 'node:fs';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ import { EXPLICIT, guessKind } from '../shared/bt_nodes.js';
9
+ import { parseXml } from '../shared/xml.js';
10
+ import { isUploadName } from './files.js';
11
+ import { errorMessage, packageShareDirectory } from './util.js';
12
+ // Beyond this, a file (or a recursion of subtrees missed) is not a tree to show
13
+ const MAX_NODES = 5000;
14
+ const PACKAGE_URL = /^package:\/\/([^/]+)\/(.+)$/;
15
+ // The XML file a widget names: a robot path (/…, ~/…), package://… or
16
+ // uploads/<name>. Throws for anything else: only XML files are read.
17
+ export function resolveTreeFile(source, uploadsDir, home = os.homedir()) {
18
+ const value = source.trim();
19
+ let file;
20
+ if (value.startsWith('uploads/')) {
21
+ const name = value.slice('uploads/'.length);
22
+ if (!isUploadName(name)) {
23
+ throw new Error(`No upload "${name}"`);
24
+ }
25
+ file = path.join(uploadsDir, name);
26
+ }
27
+ else if (PACKAGE_URL.test(value)) {
28
+ const [, name, relative] = PACKAGE_URL.exec(value);
29
+ const share = packageShareDirectory(name);
30
+ if (!share) {
31
+ throw new Error(`No package "${name}": is its workspace sourced?`);
32
+ }
33
+ file = path.resolve(share, relative);
34
+ if (!file.startsWith(share + path.sep)) {
35
+ throw new Error(`"${value}" is not in its package`);
36
+ }
37
+ }
38
+ else {
39
+ const expanded = value === '~' || value.startsWith('~/') ? path.join(home, value.slice(1)) : value;
40
+ if (!path.isAbsolute(expanded)) {
41
+ throw new Error(`Not a path: "${source}" (/…, ~/…, package://…)`);
42
+ }
43
+ file = path.resolve(expanded);
44
+ }
45
+ if (path.extname(file).toLowerCase() !== '.xml') {
46
+ throw new Error(`Not an XML file: "${source}"`);
47
+ }
48
+ return file;
49
+ }
50
+ function modelKinds(root, kinds) {
51
+ for (const model of root.children.filter((child) => child.name === 'TreeNodesModel')) {
52
+ for (const node of model.children) {
53
+ const kind = EXPLICIT[node.name];
54
+ if (kind && node.attributes.ID) {
55
+ kinds.set(node.attributes.ID, kind);
56
+ }
57
+ }
58
+ }
59
+ }
60
+ // Nav2's node models, when installed: what its nodes are
61
+ let nav2Kinds = null;
62
+ function loadNav2Kinds() {
63
+ nav2Kinds ??= (async () => {
64
+ const kinds = new Map();
65
+ const share = packageShareDirectory('nav2_behavior_tree');
66
+ if (share) {
67
+ try {
68
+ modelKinds(parseXml(await fs.readFile(path.join(share, 'nav2_tree_nodes.xml'), 'utf8')), kinds);
69
+ }
70
+ catch {
71
+ // Not there, or not readable: the known nodes and the guesses will do
72
+ }
73
+ }
74
+ return kinds;
75
+ })();
76
+ return nav2Kinds;
77
+ }
78
+ // The tree's nodes, from the main BehaviorTree; subtrees not defined, and
79
+ // recursions, are reported and left unexpanded
80
+ export function instantiate(trees, mainId, kinds, warnings) {
81
+ const nodes = [];
82
+ const visit = (element, parent, expanding) => {
83
+ if (nodes.length >= MAX_NODES) {
84
+ throw new Error(`The tree has more than ${MAX_NODES} nodes`);
85
+ }
86
+ const explicit = EXPLICIT[element.name];
87
+ const { name, ...ports } = element.attributes;
88
+ let type = element.name;
89
+ if (explicit === 'subtree') {
90
+ type = 'SubTree';
91
+ }
92
+ else if (explicit && ports.ID) {
93
+ type = ports.ID;
94
+ delete ports.ID;
95
+ }
96
+ const subtreeId = explicit === 'subtree' ? (ports.ID ?? '') : '';
97
+ const index = nodes.length;
98
+ nodes.push({
99
+ name: name ?? (subtreeId || type),
100
+ type,
101
+ kind: explicit ?? kinds.get(type) ?? guessKind(type, element.children.length),
102
+ parent,
103
+ ports,
104
+ });
105
+ if (explicit !== 'subtree') {
106
+ for (const child of element.children) {
107
+ visit(child, index, expanding);
108
+ }
109
+ return;
110
+ }
111
+ const definition = trees.get(subtreeId);
112
+ if (!subtreeId) {
113
+ warnings.push(`A SubTree without ID, line ${element.line}`);
114
+ }
115
+ else if (expanding.includes(subtreeId)) {
116
+ warnings.push(`The subtree ${subtreeId} includes itself, line ${element.line}`);
117
+ }
118
+ else if (!definition) {
119
+ warnings.push(`No BehaviorTree ${subtreeId}, for the SubTree of line ${element.line}`);
120
+ }
121
+ else if (definition.children[0]) {
122
+ visit(definition.children[0], index, [...expanding, subtreeId]);
123
+ }
124
+ };
125
+ const main = trees.get(mainId)?.children[0];
126
+ if (!main) {
127
+ throw new Error(`The BehaviorTree ${mainId} is empty`);
128
+ }
129
+ visit(main, -1, [mainId]);
130
+ return nodes;
131
+ }
132
+ // The tree of the file, its includes followed (relative to it, or to their
133
+ // ros_pkg's share directory). The main tree is the one main_tree_to_execute
134
+ // names, else the first of the file that no other uses as a subtree.
135
+ export async function loadBehaviorTree(file) {
136
+ const trees = new Map();
137
+ // The trees of the file itself, in order
138
+ const own = [];
139
+ const kinds = new Map(await loadNav2Kinds());
140
+ const warnings = [];
141
+ const files = [];
142
+ let mainId = '';
143
+ const read = async (current, top) => {
144
+ files.push(current);
145
+ let root;
146
+ try {
147
+ root = parseXml(await fs.readFile(current, 'utf8'));
148
+ }
149
+ catch (e) {
150
+ const code = e.code;
151
+ throw new Error(`Cannot read ${current}: ${code ?? errorMessage(e)}`);
152
+ }
153
+ if (root.name !== 'root') {
154
+ throw new Error(`${current} is not a behavior tree: its root element is <${root.name}>, not <root>`);
155
+ }
156
+ if (top) {
157
+ mainId = root.attributes.main_tree_to_execute ?? '';
158
+ }
159
+ modelKinds(root, kinds);
160
+ for (const child of root.children) {
161
+ if (child.name === 'include') {
162
+ const { ros_pkg: rosPackage, path: relative = '' } = child.attributes;
163
+ const share = rosPackage ? packageShareDirectory(rosPackage) : path.dirname(current);
164
+ if (!share) {
165
+ warnings.push(`Not included: ${relative}, of the package ${rosPackage}, not found`);
166
+ continue;
167
+ }
168
+ const included = path.resolve(share, relative);
169
+ if (files.includes(included)) {
170
+ continue;
171
+ }
172
+ try {
173
+ await read(included, false);
174
+ }
175
+ catch (e) {
176
+ warnings.push(`Not included: ${errorMessage(e)}`);
177
+ }
178
+ }
179
+ else if (child.name === 'BehaviorTree') {
180
+ const id = child.attributes.ID ?? (trees.size === 0 ? 'MainTree' : `BehaviorTree ${trees.size + 1}`);
181
+ if (trees.has(id)) {
182
+ warnings.push(`BehaviorTree ${id} defined twice: the first one is shown`);
183
+ continue;
184
+ }
185
+ trees.set(id, child);
186
+ if (top) {
187
+ own.push(id);
188
+ }
189
+ }
190
+ }
191
+ };
192
+ await read(file, true);
193
+ if (trees.size === 0) {
194
+ throw new Error(`${file} has no BehaviorTree`);
195
+ }
196
+ if (mainId && !trees.has(mainId)) {
197
+ warnings.push(`No BehaviorTree ${mainId}, the main_tree_to_execute`);
198
+ mainId = '';
199
+ }
200
+ if (!mainId) {
201
+ const used = new Set();
202
+ const collect = (element) => {
203
+ if (EXPLICIT[element.name] === 'subtree' && element.attributes.ID) {
204
+ used.add(element.attributes.ID);
205
+ }
206
+ element.children.forEach(collect);
207
+ };
208
+ trees.forEach(collect);
209
+ const candidates = [...own, ...trees.keys()];
210
+ mainId = candidates.find((id) => !used.has(id)) ?? candidates[0];
211
+ }
212
+ return { treeId: mainId, nodes: instantiate(trees, mainId, kinds, warnings), warnings, files };
213
+ }
214
+ // The tree a widget names (empty: none), or why it could not be read; and
215
+ // the files read, to read it again when they change
216
+ export async function readTree(source, uploadsDir, logger) {
217
+ const tree = { treeId: '', nodes: [], error: null, warnings: [] };
218
+ if (!source) {
219
+ return { tree, files: [] };
220
+ }
221
+ let files = [];
222
+ try {
223
+ const file = resolveTreeFile(source, uploadsDir);
224
+ files = [file];
225
+ const loaded = await loadBehaviorTree(file);
226
+ return { tree: { treeId: loaded.treeId, nodes: loaded.nodes, error: null, warnings: loaded.warnings }, files: loaded.files };
227
+ }
228
+ catch (e) {
229
+ logger.warn(`Behavior tree ${source}: ${errorMessage(e)}`);
230
+ return { tree: { ...tree, error: errorMessage(e) }, files };
231
+ }
232
+ }
@@ -0,0 +1,18 @@
1
+ import type { BehaviorTreeFile, BehaviorTreeModels, BehaviorTreeSaved } from '../shared/protocol.ts';
2
+ import { type Logger } from './util.ts';
3
+ export type FileState = Omit<BehaviorTreeFile, 'type' | 'id'>;
4
+ export type ModelsState = Omit<BehaviorTreeModels, 'type' | 'id'>;
5
+ export interface BehaviorTreeFileWatcher {
6
+ onFile: (file: FileState) => void;
7
+ onModels: (models: ModelsState) => void;
8
+ }
9
+ export declare function contentVersion(text: string): string;
10
+ export declare class BehaviorTreeFiles {
11
+ private readonly uploadsDir;
12
+ private readonly logger;
13
+ private readonly watched;
14
+ constructor(uploadsDir: string, logger: Logger);
15
+ watch(source: string, libraries: string[], watcher: BehaviorTreeFileWatcher): () => void;
16
+ save(source: string, text: string, base: string | null, create: boolean): Promise<BehaviorTreeSaved>;
17
+ stopAll(): void;
18
+ }