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.
- package/CHANGELOG.md +64 -0
- package/README.md +173 -6
- package/dist/server/behavior_tree_file.d.ts +17 -0
- package/dist/server/behavior_tree_file.js +232 -0
- package/dist/server/behavior_tree_files.d.ts +17 -0
- package/dist/server/behavior_tree_files.js +382 -0
- package/dist/server/behavior_tree_log.d.ts +10 -0
- package/dist/server/behavior_tree_log.js +132 -0
- package/dist/server/behavior_tree_playback.d.ts +57 -0
- package/dist/server/behavior_tree_playback.js +188 -0
- package/dist/server/behavior_tree_state.d.ts +38 -0
- package/dist/server/behavior_tree_state.js +220 -0
- package/dist/server/behavior_trees.d.ts +29 -0
- package/dist/server/behavior_trees.js +362 -0
- package/dist/server/diagnostics.d.ts +16 -0
- package/dist/server/diagnostics.js +200 -0
- package/dist/server/diagnostics_state.d.ts +23 -0
- package/dist/server/diagnostics_state.js +146 -0
- package/dist/server/disks.d.ts +14 -0
- package/dist/server/disks.js +152 -0
- package/dist/server/file_manager.d.ts +25 -0
- package/dist/server/file_manager.js +717 -0
- package/dist/server/files.d.ts +5 -2
- package/dist/server/files.js +32 -13
- package/dist/server/http.js +254 -9
- package/dist/server/netpbm.d.ts +3 -0
- package/dist/server/netpbm.js +69 -0
- package/dist/server/pty.d.ts +26 -0
- package/dist/server/pty.js +150 -0
- package/dist/server/rosbag.d.ts +25 -0
- package/dist/server/rosbag.js +418 -0
- package/dist/server/rosout.d.ts +16 -0
- package/dist/server/rosout.js +203 -0
- package/dist/server/rosout_log.d.ts +20 -0
- package/dist/server/rosout_log.js +66 -0
- package/dist/server/server.js +29 -6
- package/dist/server/session.d.ts +14 -1
- package/dist/server/session.js +130 -1
- package/dist/server/systemd.js +1 -1
- package/dist/server/systemd_format.d.ts +1 -0
- package/dist/server/systemd_format.js +19 -3
- package/dist/server/terminal_output.d.ts +25 -0
- package/dist/server/terminal_output.js +108 -0
- package/dist/server/terminals.d.ts +32 -0
- package/dist/server/terminals.js +366 -0
- package/dist/server/util.d.ts +4 -0
- package/dist/server/util.js +45 -1
- package/dist/shared/bt_nodes.d.ts +25 -0
- package/dist/shared/bt_nodes.js +180 -0
- package/dist/shared/file_manager.d.ts +130 -0
- package/dist/shared/file_manager.js +7 -0
- package/dist/shared/protocol.d.ts +242 -2
- package/dist/shared/protocol.js +12 -0
- package/dist/shared/xml.d.ts +25 -0
- package/dist/shared/xml.js +164 -0
- package/dist/types/client/api.d.ts +26 -3
- package/dist/types/client/connection.d.ts +52 -1
- package/dist/types/client/dialogs.d.ts +2 -1
- package/dist/types/client/icons.d.ts +51 -0
- package/dist/types/client/sdk/index.d.ts +5 -3
- package/dist/types/client/widgets/types.d.ts +5 -1
- package/dist/types/shared/bt_nodes.d.ts +25 -0
- package/dist/types/shared/file_manager.d.ts +130 -0
- package/dist/types/shared/protocol.d.ts +242 -2
- package/dist/www/assets/Joystick.js +1 -1
- package/dist/www/assets/RobotModelView.js +2 -2
- package/dist/www/assets/TerminalView.css +1 -0
- package/dist/www/assets/TerminalView.js +17 -0
- package/dist/www/assets/dialogs.js +1 -0
- package/dist/www/assets/icons.js +1 -1
- package/dist/www/assets/index.css +1 -1
- package/dist/www/assets/index.js +69 -2
- package/dist/www/assets/keys.js +3 -0
- package/dist/www/assets/shared/tassello-sdk.js +1 -1
- package/dist/www/index.html +2 -0
- package/docs/plugins.md +28 -0
- package/package.json +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,70 @@
|
|
|
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
|
+
- Diagnostics widget: the robot's diagnostics (`diagnostic_msgs/msg/DiagnosticArray`), from every
|
|
32
|
+
node's `diagnostic_updater` (`/diagnostics`, by node) or the aggregator's tree
|
|
33
|
+
(`/diagnostics_agg`): each status's level, message, hardware ID and values, the groups with the
|
|
34
|
+
worst under them, as a tree, as a list or as the latest changes of level, filtered by level and
|
|
35
|
+
text. A widget can be for some statuses only (`Sensors, motor_*`). The server follows the
|
|
36
|
+
diagnostics for every page, marks stale the statuses no longer received, and keeps their changes
|
|
37
|
+
of level.
|
|
38
|
+
- Plugins: `connection.subscribeDiagnostics`, and the `DiagnosticsIcon` and `UnfoldIcon` icons.
|
|
39
|
+
- File Manager widget: the robot's files, in the behavior tree editor's look. Folders listed with
|
|
40
|
+
their entries' sizes and times, unfolded in place, sorted and filtered; places (home, the ROS
|
|
41
|
+
workspaces sourced, bookmarks) and the disks with their free space; an entry's details, its
|
|
42
|
+
permissions changed there; an overview of the disks and of what takes a folder's space (its
|
|
43
|
+
largest entries, its files by type, the largest and latest files). Entries made, renamed in
|
|
44
|
+
place, cut, copied and pasted, moved or copied by dragging them onto folders, deleted,
|
|
45
|
+
downloaded (folders as `.tar.gz`), uploaded by dropping files and folders onto the widget, with
|
|
46
|
+
their progress; names taken are asked about. Text files are edited in tabs, as gedit does: line
|
|
47
|
+
numbers, syntax highlighting (YAML, XML, Python, C++, CMake, JSON, shell, INI, Markdown, ROS
|
|
48
|
+
interfaces), search and replace, indentation, line ends kept, undo and redo, never saved over
|
|
49
|
+
changes made on the robot since they were read. Images are shown, PGM maps too. A widget can
|
|
50
|
+
stay in its folder, or be read-only. The server polls the folders and files shown.
|
|
51
|
+
- Plugins: `fsApi` and `ApiError`, the `Fs*` types, and the `FolderIcon`, `FolderPlusIcon`,
|
|
52
|
+
`FileIcon`, `FileTextIcon`, `FilePlusIcon`, `UploadIcon`, `ArrowUpIcon`, `ArrowLeftIcon`,
|
|
53
|
+
`HomeIcon`, `HardDriveIcon`, `BookmarkIcon`, `SidebarIcon`, `PieChartIcon`, `InfoIcon`,
|
|
54
|
+
`WrapIcon`, `LinkIcon`, `LockIcon` and `ArchiveIcon` icons.
|
|
55
|
+
- REST errors of the API reject with an `ApiError`, which has the HTTP status.
|
|
56
|
+
- Terminal widget: a shell on the robot (the login shell of the server's user), or a command, in
|
|
57
|
+
a full terminal (xterm.js): colours, full screen programs and their mouse, copy and paste. It
|
|
58
|
+
outlives the page: reloaded, reconnected or shown again, it is as it was, full screen programs
|
|
59
|
+
included; hung up an hour after no page shows it. On touch screens, Termux's extra keys (Esc,
|
|
60
|
+
Tab, Ctrl, Alt, Home, End, the arrows, Page Up and Down): sticky modifiers that also apply to the
|
|
61
|
+
on-screen keyboard, keys swiped up for another character, a layout of one's own; the terminal
|
|
62
|
+
fills the screen above the on-screen keyboard while typing; one finger scrolls, two pinch the
|
|
63
|
+
text's size. Needs Python 3 (ROS 2's), no native module. Terminals open from the dashboard's own
|
|
64
|
+
pages only.
|
|
65
|
+
- Plugins: `connection.subscribeTerminal`, `terminalInput`, `resizeTerminal`, `ackTerminal`,
|
|
66
|
+
`restartTerminal`, the `TerminalOutput` and `TerminalExit` types, and the `ShellIcon`,
|
|
67
|
+
`KeyboardIcon`, `MaximizeIcon`, `MinimizeIcon`, `ClipboardIcon`, `TextSmallerIcon` and
|
|
68
|
+
`TextLargerIcon` icons.
|
|
69
|
+
|
|
6
70
|
## 0.1.0
|
|
7
71
|
|
|
8
72
|
The first release, from the Dieletrikbot's web dashboard.
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Tassello
|
|
2
2
|
|
|
3
3
|
**A web dashboard for ROS 2 robots.** Drive the robot, send it navigation goals, see it move in 3D,
|
|
4
|
-
watch its cameras, run commands on it and manage its services from any
|
|
5
|
-
phones included. Arrange it all in the browser, as panels of widgets, and extend it with plugins.
|
|
4
|
+
watch its cameras, run commands on it, open a terminal on it and manage its services from any
|
|
5
|
+
browser on the network, phones included. Arrange it all in the browser, as panels of widgets, and extend it with plugins.
|
|
6
6
|
|
|
7
7
|
*Tassello* is Italian for a mosaic tile, and for a wall plug: the dashboard is a mosaic of widgets,
|
|
8
8
|
and plugins add more.
|
|
@@ -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
|
|
13
|
+
images, command buttons, a systemd application manager, a behavior tree inspector and
|
|
14
|
+
editor, the ROS log of every node, the robot's diagnostics, a file manager and a terminal.
|
|
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
|
|
|
@@ -20,7 +21,8 @@ Tassello runs on the robot (or any machine on its ROS network) and needs:
|
|
|
20
21
|
|
|
21
22
|
- ROS 2: Humble, Jazzy, Kilted or Lyrical, on x64 or arm64 (e.g. a Raspberry Pi), for which
|
|
22
23
|
[rclnodejs](https://github.com/RobotWebTools/rclnodejs) ships prebuilt binaries;
|
|
23
|
-
- Node.js 22 or newer, and npm
|
|
24
|
+
- Node.js 22 or newer, and npm;
|
|
25
|
+
- Python 3, which ROS 2 comes with: the Terminal widget's pseudo-terminals.
|
|
24
26
|
|
|
25
27
|
```bash
|
|
26
28
|
npm install -g tassello
|
|
@@ -113,6 +115,12 @@ after the others on the devices already arranged.
|
|
|
113
115
|
| **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
116
|
| **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
117
|
| **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 |
|
|
118
|
+
| **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 |
|
|
119
|
+
| **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 |
|
|
120
|
+
| **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 |
|
|
121
|
+
| **Diagnostics** | The robot's diagnostics (`/diagnostics`, or the aggregator's `/diagnostics_agg`), as rqt_robot_monitor shows them: each status's level, the groups with the worst under them, those with problems unfolded. As a tree, as a list (the worst first) or as the latest changes of level, filtered by level and by text; a widget can be for some statuses only. A status's details: its message, hardware ID, values and latest changes. Pause, download, and forget the statuses no longer published |
|
|
122
|
+
| **File Manager** | The robot's files: folders unfolded in place, places (home, the ROS workspaces, bookmarks) and the disks' free space, an entry's details and permissions, what takes a folder's space. Make, rename, cut, copy, paste, drag onto folders, delete, download (folders as `.tar.gz`) and upload (drop files and folders onto it). Text files edited in tabs as gedit does: highlighting, search and replace, undo; images and PGM maps shown |
|
|
123
|
+
| **Terminal** | A shell on the robot, or a command (`htop`, `tmux attach`), in a full terminal: colours, full screen programs, the mouse. It outlives the page: reloaded, reconnected or shown again, it is as it was. On touch screens, the keys phones lack, as Termux has them: Esc, Tab, Ctrl, Alt, Home, End, the arrows, Page Up and Down, and while typing the terminal fills the screen above the on-screen keyboard |
|
|
116
124
|
|
|
117
125
|
Their properties (topics, frames, speeds, colours, ...) are edited in the side panel of edit mode.
|
|
118
126
|
|
|
@@ -153,6 +161,141 @@ Notes on some of them:
|
|
|
153
161
|
- **Image Viewer** reads only images (PNG, JPEG, GIF, WebP, BMP, SVG). Uploads are kept next to
|
|
154
162
|
the panels (`~/.local/share/tassello/images`). Images are served sandboxed: an SVG cannot run
|
|
155
163
|
scripts in the dashboard.
|
|
164
|
+
- **Behavior Tree Inspector** follows a `nav2_msgs/BehaviorTreeLog` topic, which BT.CPP trees
|
|
165
|
+
log with Nav2's `RosTopicLogger`: Nav2's bt_navigator on `/behavior_tree_log`. Its *tree file*
|
|
166
|
+
is the XML the robot runs (`/…`, `~/…`, `package://…`, browsed or uploaded), read with its
|
|
167
|
+
includes and subtrees, and again when it changes; e.g. Nav2's default,
|
|
168
|
+
`package://nav2_bt_navigator/behavior_trees/navigate_to_pose_w_replanning_and_recovery.xml`.
|
|
169
|
+
Without it, the nodes are listed as the log names them. Since Jazzy, the log gives the nodes'
|
|
170
|
+
UIDs, which tell them apart exactly; before, nodes of the same name (a `RecoveryNode` named
|
|
171
|
+
after its action) are told apart by their statuses and their parents'. The log only says what
|
|
172
|
+
changes: the server follows it for every page, and for 10 minutes after the last one closes,
|
|
173
|
+
so that a page opened later sees the nodes as they are. A node reset to idle by its parent
|
|
174
|
+
shows how it last ended.
|
|
175
|
+
|
|
176
|
+
Its record button records the log on the robot with `ros2 bag record`, into a new folder
|
|
177
|
+
(`<recordings folder>/behavior_tree_<date>_<time>` by default), until pressed again: every
|
|
178
|
+
page showing the topic shows it recording. Its playback button opens a rosbag of the robot
|
|
179
|
+
(a bag's folder, or its `.mcap` or `.db3` file, e.g. of a whole run recorded with
|
|
180
|
+
`ros2 bag record -a`) and plays its log in the widget, on the tree of its file, without
|
|
181
|
+
publishing anything: pause, seek and choose the speed (0.1× to 16×). Bags of Humble and of
|
|
182
|
+
Jazzy are read either way; SQLite bags need Node.js 22.13, zstd compressed MCAP 22.15.
|
|
183
|
+
- **Behavior Tree Editor** edits one BehaviorTree of the file at a time (its toolbar switches
|
|
184
|
+
between them, adds new ones, and double clicking a subtree opens its tree, even in an included
|
|
185
|
+
file). Add a node from the palette by dragging it where it goes (into a node, or beside one), or
|
|
186
|
+
click it to add it into the selected node, or after it. Drag nodes to move them with their
|
|
187
|
+
children. The side panel edits the selected node's name and ports, with their types, defaults
|
|
188
|
+
and descriptions from its model, and its other attributes (BT.CPP 4's `_skipIf`, `_while`…);
|
|
189
|
+
it moves, duplicates, wraps, unwraps and replaces it. What BT.CPP would refuse, such as a
|
|
190
|
+
decorator without its child, is marked. Keys: Delete, Ctrl+Z, Ctrl+Shift+Z, Ctrl+C, Ctrl+X,
|
|
191
|
+
Ctrl+V, Ctrl+D, Ctrl+S, the arrows to move the selection (Alt: the node).
|
|
192
|
+
|
|
193
|
+
The palette has BT.CPP's nodes (of the file's format, `BTCPP_format`), Nav2's (its
|
|
194
|
+
`nav2_tree_nodes.xml`, when installed), those of the file's `TreeNodesModel` and includes, and
|
|
195
|
+
those of the *node libraries*: XML files of `TreeNodesModel`, as Groot exports them, set in the
|
|
196
|
+
widget's properties or loaded from the palette. Saving writes back only the elements changed,
|
|
197
|
+
the rest of the file (comments, blank lines, indentation) as it was. The file is read again when
|
|
198
|
+
it changes on the robot: without changes in the editor it is shown at once, with changes the
|
|
199
|
+
editor asks which to keep, and a save over a file changed since it was read is refused.
|
|
200
|
+
Uploads, and files the server cannot write, are saved elsewhere with *Save as*. The editor keeps
|
|
201
|
+
its changes while the page is open, and the page asks before closing with changes not saved.
|
|
202
|
+
- **Logs** follows `/rosout` (`rcl_interfaces/msg/Log`), on which every node's logger publishes.
|
|
203
|
+
Its list of nodes has those running in the ROS graph and those that logged, with their
|
|
204
|
+
warnings and errors: choose some to see only theirs. A node's logger is named after it, dots
|
|
205
|
+
for slashes (`robot1.amcl` for `/robot1/amcl`). The level buttons show and hide each level
|
|
206
|
+
(Shift+click: that level only); *Include* and *Exclude* keep or drop the messages containing any
|
|
207
|
+
of their terms, separated by `;`, or matching a regular expression. Pausing holds the list while
|
|
208
|
+
the new messages wait; clearing empties it for this page. A message clicked shows whole, with its
|
|
209
|
+
file, line and function; the arrow keys go through the list. *Download* saves the messages shown
|
|
210
|
+
as ROS prints them (`[WARN] [1729435407.512346] [robot1.amcl]: ...`). The widget's properties
|
|
211
|
+
set its nodes (names or globs, e.g. `amcl, *_server`: one widget per subsystem), the levels and
|
|
212
|
+
filters it opens with, and the time shown: the clock, ROS stamps, or none. The server follows the
|
|
213
|
+
log for every page, keeping its latest 10,000 messages, and for 10 minutes after the last page
|
|
214
|
+
closes: a page opened later sees what was logged meanwhile, and when first opened, what each
|
|
215
|
+
node logged in its last 10 seconds (rosout's own history).
|
|
216
|
+
- **Diagnostics** follows a `diagnostic_msgs/msg/DiagnosticArray` topic: `/diagnostics`, on which
|
|
217
|
+
every node's `diagnostic_updater` publishes its statuses (named `node: task`, grouped by node),
|
|
218
|
+
or `/diagnostics_agg`, the aggregator's tree of them (`/Robot/Sensors/Lidar`). A group shows the
|
|
219
|
+
worst level under it; one the aggregator puts in error only because some statuses under it are
|
|
220
|
+
stale shows as stale. A status not received for 5 s, or for 3 of its periods if it is slower, is
|
|
221
|
+
stale: what published it stopped. The level buttons show and hide each level (Shift+click: that
|
|
222
|
+
level only), counting the statuses, not the groups that sum them up; *Filter* keeps the statuses
|
|
223
|
+
whose name, message or hardware ID contain any of its terms, separated by `;`, or match a
|
|
224
|
+
regular expression, and a group's with all those under it. The groups with problems unfold, and
|
|
225
|
+
stay unfolded once these are gone, until folded; the arrow keys go through the tree and fold
|
|
226
|
+
and unfold it. A status clicked shows its message, hardware ID, values and latest changes of
|
|
227
|
+
level. Pausing holds the statuses as they are; *Forget* hides those no longer published, until
|
|
228
|
+
they are again; *Download* saves the statuses shown, the worst first, with their values (or the
|
|
229
|
+
changes shown). The widget's properties set its statuses (names, paths or globs, e.g.
|
|
230
|
+
`Sensors, motor_*`: from the root if they start with `/`, else at any depth), and the levels,
|
|
231
|
+
filter and view it opens with. The server follows the diagnostics for every page, and for 10
|
|
232
|
+
minutes after the last page closes, keeping the latest status of each name and their latest
|
|
233
|
+
1,000 changes of level.
|
|
234
|
+
- **File Manager** opens its *folder* (`~` by default; `/…`, `~/…`, `package://…`) and lists it
|
|
235
|
+
with its entries' sizes and times, sorted by name (naturally: `run_2` before `run_10`), size or
|
|
236
|
+
time, folders first; the chevrons unfold folders in place. Its places are home, the root, the
|
|
237
|
+
ROS installations and workspaces sourced (`AMENT_PREFIX_PATH`), its *bookmarks* (the widget's,
|
|
238
|
+
and those made in the page, kept by the browser) and the disks, with their free space. Click to
|
|
239
|
+
select (Ctrl, Shift: several), double click (on touch screens tap) to open: folders, text files
|
|
240
|
+
into the editor, images (PNG, JPEG, SVG…, and PGM maps, converted on the robot) into a viewer.
|
|
241
|
+
Right click, or the ⋯ of a row, for the rest: download (a folder, or several entries, as one
|
|
242
|
+
`.tar.gz`), cut, copy and paste (between file managers too), duplicate, move or copy to a folder
|
|
243
|
+
picked, rename (F2, in place), copy the path, details, delete (there is no trash: it asks
|
|
244
|
+
first). Drag entries onto a folder, a place or the address bar's folders to move them (Ctrl:
|
|
245
|
+
copy them); drop files or folders from the computer to upload them, with their progress. Where
|
|
246
|
+
names are taken, it asks: replace, keep both (`map (2).yaml`) or skip. *Details* show an entry's
|
|
247
|
+
kind, size (a folder's computed on demand), times, owner, mode, link and disk, an image's
|
|
248
|
+
preview, and change its permissions; *what takes the space* shows the disks (size, use, free
|
|
249
|
+
space, inodes when short) and analyzes the folder: its largest entries, its files by type, the
|
|
250
|
+
largest and the latest files under it, on its disk only, for 15 s at most. Keys: the arrows
|
|
251
|
+
(Left and Right fold), Enter, Backspace, Alt+arrows (up, back, forward), Delete, F2, Ctrl+A,
|
|
252
|
+
Ctrl+C, Ctrl+X, Ctrl+V, Ctrl+F (filter), Ctrl+L (type a path), Ctrl+H (hidden files), and typing
|
|
253
|
+
a name's first letters.
|
|
254
|
+
|
|
255
|
+
Text files (UTF-8, 5 MB at most) open in tabs, edited as with gedit: line numbers, the line of
|
|
256
|
+
the cursor lit, syntax highlighting (YAML, XML for URDF, xacro, launch and SDF, Python, C and
|
|
257
|
+
C++, CMake, JSON, shell, INI, TOML and systemd units, Markdown, ROS interfaces), search and
|
|
258
|
+
replace (case, whole words, regular expressions with `$1`), Tab and Shift+Tab indenting the lines
|
|
259
|
+
selected, new lines indented as the one before, Ctrl+/ commenting lines out, go to line (Ctrl+I),
|
|
260
|
+
undo and redo a word at a time, long lines wrapped on demand (then not highlighted). The status
|
|
261
|
+
line shows the cursor's line and column, and switches the highlighting, the indentation and the
|
|
262
|
+
line ends (kept as the file has them, LF or CRLF). As the behavior tree editor, it saves only
|
|
263
|
+
over the version read: a file changed on the robot is shown at once without changes in the
|
|
264
|
+
editor, with changes the editor asks which to keep, and a save over a file changed since it was
|
|
265
|
+
read is refused. *Save as* writes elsewhere, e.g. a file the server's user cannot write. Changes
|
|
266
|
+
are kept while the page is open, and the page asks before closing with changes not saved.
|
|
267
|
+
|
|
268
|
+
The folders shown and the files open are read again every 3 s. A widget can *stay in its
|
|
269
|
+
folder* (e.g. one for the maps, or the rosbags) or be *read-only*: these are the page's choices,
|
|
270
|
+
not a protection (see Security).
|
|
271
|
+
- **Terminal** runs the login shell of the server's user (`$SHELL -l`, as SSH does: its
|
|
272
|
+
`~/.bashrc` and the ROS setup Tassello was started with), or the widget's *command* through it,
|
|
273
|
+
in its *folder* (`~` by default; `/…`, `~/…`, `package://…`), on a pseudo-terminal of its own:
|
|
274
|
+
`TERM=xterm-256color`, true colour, full screen programs (vim, htop, less) and their mouse. A
|
|
275
|
+
terminal belongs to the browser's tab: reloading the page, losing the connection or showing
|
|
276
|
+
another panel and coming back finds it as it was, what it printed meanwhile included, screens of
|
|
277
|
+
full screen programs too. A terminal no page shows is hung up after an hour (its programs get
|
|
278
|
+
SIGHUP, as when a terminal window is closed), as is the one unseen the longest when 16 are open:
|
|
279
|
+
for jobs that should outlive it, `tmux` or `nohup`. A *command* such as `tmux new -A -s robot` gives every page, and every
|
|
280
|
+
device, the same session. When the program ends, Enter or *Restart* starts it again; *Restart*
|
|
281
|
+
also hangs up a program that hangs. Changing the widget's command or folder starts it anew.
|
|
282
|
+
|
|
283
|
+
Copy with Ctrl+C while text is selected (it interrupts otherwise) or Ctrl+Shift+C, paste with
|
|
284
|
+
Ctrl+Shift+V or the toolbar's button (pages served over http cannot read the clipboard: it opens
|
|
285
|
+
a box to paste into, on phones too). The toolbar changes the size of the text for this device, as
|
|
286
|
+
pinching does, and fills the window with the terminal.
|
|
287
|
+
|
|
288
|
+
On touch screens (or always, or never: *Extra keys*) the keys phones lack are under the
|
|
289
|
+
terminal, as Termux has them: Esc, `/`, `-`, Home, ↑, End, Page Up, then Tab, Ctrl, Alt, ←, ↓,
|
|
290
|
+
→, Page Down. Tapped, they type without closing the on-screen keyboard. Ctrl, Alt and Shift
|
|
291
|
+
apply to the next key, of the row or of the keyboard (Ctrl, then `c`: interrupt); tapped twice,
|
|
292
|
+
or held, they stay until tapped again. Swiping up on a key, or holding it, types what its corner
|
|
293
|
+
shows (`\`, `|`, `~`); the arrows repeat while held. Their *layout* is a property: a line per
|
|
294
|
+
row, keys by name (`ESC TAB CTRL ALT SHIFT HOME END PGUP PGDN UP DOWN LEFT RIGHT INS DEL BKSP
|
|
295
|
+
ENTER SPACE F1`…`F12 KEYBOARD PASTE`) or what they type (`~`, `|`, `ros2`), what swiping up
|
|
296
|
+
types after a colon (`-:|`). While the on-screen keyboard is open, the terminal fills the screen
|
|
297
|
+
above it, the keys right above the keyboard (*Full screen while typing*). One finger scrolls (the
|
|
298
|
+
scrollback, or the program: less, htop), flung or not; two pinch the text's size.
|
|
156
299
|
|
|
157
300
|
## Plugins
|
|
158
301
|
|
|
@@ -180,8 +323,14 @@ and ship example panels. See **[docs/plugins.md](docs/plugins.md)**.
|
|
|
180
323
|
## Security
|
|
181
324
|
|
|
182
325
|
Tassello has no login. **Anyone who can open the page can drive the robot, run any command as
|
|
183
|
-
the server's user
|
|
184
|
-
|
|
326
|
+
the server's user (the Terminal, a shell), and read, write, upload, move and delete any of the
|
|
327
|
+
files that user can (the File Manager)**: keep it on a network you trust, or behind a reverse proxy
|
|
328
|
+
with authentication (it serves everything under `/`, its WebSocket at `/ws`). Changes to the files
|
|
329
|
+
are accepted from the dashboard's own pages only (they send a header that another site's page
|
|
330
|
+
cannot), and the root, the home folder and the system's top folders (`/usr`, `/etc`…) are never
|
|
331
|
+
deleted nor moved. Terminals open from the dashboard's own pages only too: those whose origin is
|
|
332
|
+
the host they reached (the `Host` header, or the `X-Forwarded-Host` a reverse proxy sets), which
|
|
333
|
+
another site's page cannot pretend.
|
|
185
334
|
|
|
186
335
|
## How it works
|
|
187
336
|
|
|
@@ -192,6 +341,7 @@ proxy with authentication (it serves everything under `/`, its WebSocket at `/ws
|
|
|
192
341
|
├─ rclnodejs: ROS
|
|
193
342
|
├─ sharp: raw images to JPEG, maps to PNG
|
|
194
343
|
├─ bash: the buttons' commands
|
|
344
|
+
├─ python3: the terminals' pseudo-terminals
|
|
195
345
|
└─ the plugins' server sides
|
|
196
346
|
```
|
|
197
347
|
|
|
@@ -207,7 +357,24 @@ Safety and bandwidth are the server's concern:
|
|
|
207
357
|
of their publishers, so latched maps arrive.
|
|
208
358
|
- Topics are subscribed once, whatever the number of pages, with the type and QoS of their
|
|
209
359
|
publishers; TF only while a page shows the robot.
|
|
360
|
+
- Behavior tree logs, streams of transitions, are followed whole, none dropped: each page gets
|
|
361
|
+
the nodes that changed, at most 10 times a second, however fast the tree ticks.
|
|
362
|
+
- The ROS log is followed once for every page: each gets the new messages at most 10 times a
|
|
363
|
+
second, at most 500 at a time (a node logging faster has some skipped, which the page shows).
|
|
364
|
+
Only the rows in sight are drawn.
|
|
365
|
+
- Diagnostics are followed once for every page: each gets the statuses that changed, at most 4
|
|
366
|
+
times a second, not those received the same again. Subscribed best effort, they come from
|
|
367
|
+
every publisher, whatever its QoS.
|
|
210
368
|
- A button runs the command saved in its panel, never one sent by the page.
|
|
369
|
+
- A terminal's pseudo-terminal is opened by a few lines of Python (ROS 2's own), no native module
|
|
370
|
+
to build. Its output is also played on a terminal of the server's own (xterm.js, headless), which
|
|
371
|
+
paces the program, never the pages: a page gets at most 256 KB ahead of what it has drawn, and a
|
|
372
|
+
page further behind (a slow phone, a hidden tab, a flood of output) gets the screen as it is
|
|
373
|
+
instead of all that led to it. A page that reconnects gets what it missed, from the last 1 MB.
|
|
374
|
+
- The File Manager's folders are read again every 3 s, and answered "unchanged" while they are;
|
|
375
|
+
the largest are listed in part (5,000 entries), and only the rows in sight are drawn. Uploads
|
|
376
|
+
and downloads are streamed, folders archived by `tar` as they are sent; saves replace files at
|
|
377
|
+
once (a temporary file renamed over them).
|
|
211
378
|
|
|
212
379
|
## Developing
|
|
213
380
|
|
|
@@ -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,17 @@
|
|
|
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 class BehaviorTreeFiles {
|
|
10
|
+
private readonly uploadsDir;
|
|
11
|
+
private readonly logger;
|
|
12
|
+
private readonly watched;
|
|
13
|
+
constructor(uploadsDir: string, logger: Logger);
|
|
14
|
+
watch(source: string, libraries: string[], watcher: BehaviorTreeFileWatcher): () => void;
|
|
15
|
+
save(source: string, text: string, base: string | null, create: boolean): Promise<BehaviorTreeSaved>;
|
|
16
|
+
stopAll(): void;
|
|
17
|
+
}
|