tassello 0.6.0 → 0.8.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 +27 -0
- package/README.md +62 -9
- package/dist/server/behavior_tree_playback.js +2 -2
- package/dist/server/behavior_trees.d.ts +5 -3
- package/dist/server/behavior_trees.js +33 -35
- package/dist/server/rosbag.d.ts +15 -11
- package/dist/server/rosbag.js +396 -144
- package/dist/server/rosbag_format.d.ts +28 -0
- package/dist/server/rosbag_format.js +366 -0
- package/dist/server/rosbag_process.d.ts +20 -0
- package/dist/server/rosbag_process.js +99 -0
- package/dist/server/rosbags.d.ts +42 -0
- package/dist/server/rosbags.js +444 -0
- package/dist/server/server.js +9 -2
- package/dist/server/session.d.ts +4 -0
- package/dist/server/session.js +17 -1
- package/dist/server/system.d.ts +40 -0
- package/dist/server/system.js +606 -0
- package/dist/server/system_format.d.ts +51 -0
- package/dist/server/system_format.js +264 -0
- package/dist/shared/protocol.d.ts +39 -2
- package/dist/shared/rosbag.d.ts +104 -0
- package/dist/shared/rosbag.js +35 -0
- package/dist/shared/system.d.ts +113 -0
- package/dist/shared/system.js +45 -0
- package/dist/types/client/connection.d.ts +18 -2
- package/dist/types/client/icons.d.ts +1 -0
- package/dist/types/client/sdk/index.d.ts +3 -1
- package/dist/types/shared/protocol.d.ts +39 -2
- package/dist/types/shared/rosbag.d.ts +104 -0
- package/dist/types/shared/system.d.ts +113 -0
- package/dist/www/assets/Joystick.js +1 -1
- package/dist/www/assets/RobotModelView.js +1 -1
- package/dist/www/assets/TerminalView.js +2 -2
- package/dist/www/assets/dialogs.js +1 -1
- package/dist/www/assets/icons.js +1 -1
- package/dist/www/assets/index.css +1 -1
- package/dist/www/assets/index.js +48 -47
- package/dist/www/assets/shared/tassello-sdk.js +1 -1
- package/docs/plugins.md +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,33 @@ change the plugin API (`tassello/sdk`, `tassello/server`, `tassello/vite`), as n
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
- System Monitor widget: the robot's computer, every second. The CPU's load, of each core (those
|
|
9
|
+
offline too), their frequencies and the load average; the memory, its cache and the swap; the
|
|
10
|
+
temperatures of the thermal zones and hwmon chips, by chip with their hottest, against their
|
|
11
|
+
own limits; the fans; a Raspberry Pi's throttling (under-voltage, frequency capped, throttled,
|
|
12
|
+
soft temperature limit, now and since boot); the GPUs: a Jetson's (devfreq), NVIDIA's
|
|
13
|
+
(`nvidia-smi`), AMD's (amdgpu); the power rails: a Jetson's INA3221 (L4T 32's too), hwmon's
|
|
14
|
+
power sensors, Intel's RAPL when readable; the disks' traffic and busy time, and the file
|
|
15
|
+
systems' space; the networks' traffic, of the device's own interfaces or those chosen. Charts
|
|
16
|
+
of the last 5 minutes, kept by the server, with the values at the pointer (or the arrow keys).
|
|
17
|
+
The board, its OS, uptime, and a Jetson's L4T release and power mode.
|
|
18
|
+
- Plugins: `connection.subscribeSystem`, and the types of `src/shared/system.ts`.
|
|
19
|
+
- Rosbags widget: the robot's rosbags, as `ros2 bag` makes and reads them. A folder's bags, the
|
|
20
|
+
newest first, with their time span, size, storage and compression, and each topic's type,
|
|
21
|
+
count and rate, from their `metadata.yaml` (Humble's and Jazzy's), else from their files (a
|
|
22
|
+
recording killed: MCAP's summary, SQLite's tables). Downloaded (folders as `.tar.gz`), deleted
|
|
23
|
+
(bags only, from the dashboard's own pages), or played on the robot's topics with
|
|
24
|
+
`ros2 bag play`: paused, sought, sped up, looped, from an offset, some topics only, `/clock`
|
|
25
|
+
published. New ones recorded with `ros2 bag record`: topics chosen from the graph, all of them,
|
|
26
|
+
or matching a regular expression, but those matching another; with its advanced options, the
|
|
27
|
+
storage (MCAP, SQLite) and its preset, a new file by size or duration, the compression of each
|
|
28
|
+
file or message and its format, the cache, hidden topics, sim time. The recordings and the
|
|
29
|
+
playback are the robot's: every page sees them, and stops them.
|
|
30
|
+
- Behavior Tree Inspector: its recordings are made by the same rosbags service, with the same
|
|
31
|
+
advanced options, and listed (and stopped) by the Rosbags widgets. Bags compressed by rosbag2
|
|
32
|
+
(zstd, each file or each message) are played back too.
|
|
33
|
+
- Plugins: `connection.subscribeBags`, `bagsAction`, `recordBehaviorTree`'s options, and the
|
|
34
|
+
types of `src/shared/rosbag.ts`.
|
|
8
35
|
- Parameters widget: nodes' parameters, as rqt_reconfigure and `ros2 param` show them. A node's
|
|
9
36
|
parameters grouped by their prefixes, each edited in place (a switch, a slider within its range,
|
|
10
37
|
a field), with what the node refuses and why; changes made elsewhere followed live on
|
package/README.md
CHANGED
|
@@ -12,8 +12,10 @@ and plugins add more.
|
|
|
12
12
|
- **Built in**: teleop pads, a Nav2 map with goals, the robot's URDF in 3D, an arm jogger, cameras,
|
|
13
13
|
images, command buttons, a systemd application manager, a behavior tree inspector and
|
|
14
14
|
editor, the ROS log of every node, the robot's diagnostics, its topics, services and actions
|
|
15
|
-
as the `ros2` command line shows them, its nodes' parameters,
|
|
16
|
-
|
|
15
|
+
as the `ros2` command line shows them, its nodes' parameters, its rosbags (recorded, listed,
|
|
16
|
+
downloaded and played back), a file manager, a terminal, its networks (Wi-Fi
|
|
17
|
+
joined, connections switched, the robot as an access point), and its computer: CPU, memory,
|
|
18
|
+
temperatures, a Raspberry Pi's throttling, a Jetson's GPU and power, disks and networks.
|
|
17
19
|
- **Plugins**: widgets of your own, with server-side code if they need it, as npm or ROS packages.
|
|
18
20
|
- **One process**: a ROS 2 node in Node.js. Nothing to install on the robot but Node.js.
|
|
19
21
|
|
|
@@ -132,8 +134,10 @@ after the others on the devices already arranged.
|
|
|
132
134
|
| **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 |
|
|
133
135
|
| **ROS Monitor** | The ROS graph, as the `ros2` command line shows it, on three sheets: its topics, services and actions, filtered by name or type, the hidden ones on demand. A topic's messages, their rate, bandwidth and delay, its publishers and subscriptions with their QoS and those that cannot talk, a message to publish once or at a rate (`ros2 topic echo`, `hz`, `bw`, `delay`, `info -v`, `pub`); a service to call, once or at a rate (`ros2 service call`, `info`, `echo`); every goal of an action, whoever sent it, with its feedback, and a goal to send or cancel (`ros2 action send_goal`, `info`); their types' definitions (`ros2 interface show`) |
|
|
134
136
|
| **Parameters** | Nodes' parameters, as rqt_reconfigure and `ros2 param` show them: the nodes of the graph, and the parameters of the one chosen grouped by their prefixes, each edited in place (a switch, a slider within its range, a field), live as they change wherever they are set. Their descriptions, ranges and constraints (`ros2 param describe`); new ones set and undeclared ones deleted (`set`, `delete`); all of them dumped as YAML, downloaded or saved on the robot (`dump`), and loaded from a YAML file of the robot or of the page's device (`load`) |
|
|
137
|
+
| **Rosbags** | The robot's rosbags, as `ros2 bag` makes and reads them: those of a folder, the newest first, with when, how long and how big, their storage and compression, each topic's type, count and rate (`ros2 bag info`); downloaded, deleted, or played on the robot's topics (`ros2 bag play`: paused, sought, sped up, looped, some topics only). New ones recorded (`ros2 bag record`): topics chosen from the graph, all of them, or matching an expression, with rosbag2's options: the storage (MCAP, SQLite) and its preset, a new file by size or duration, compression of each file or message. Every page sees the recordings and the playback, the behavior tree inspectors' too |
|
|
135
138
|
| **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 |
|
|
136
139
|
| **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 |
|
|
140
|
+
| **System Monitor** | The robot's computer, as top, sensors and a Jetson's jtop show it: the CPU's load, of each core, and their frequencies; the memory and swap; the temperatures by chip, and the fans; a Raspberry Pi's throttling (under-voltage, capped frequency, now and since boot); the GPUs (a Jetson's, NVIDIA's, AMD's) and the power rails (a Jetson's); the disks' traffic and space; the networks' traffic. With charts of the last 5 minutes |
|
|
137
141
|
| **Networking** | The robot's networks, through NetworkManager, as a phone's settings show them: the Wi-Fi networks in range, joined with their password; the connections saved, switched between, edited (their addresses by DHCP or static), forgotten; the robot as an access point, its network shared by a QR code; the interfaces, their state and addresses. Asks before a change that would cut the page off from the robot |
|
|
138
142
|
|
|
139
143
|
Their properties (topics, frames, speeds, colours, ...) are edited in the side panel of edit mode.
|
|
@@ -198,12 +202,14 @@ Notes on some of them:
|
|
|
198
202
|
shows how it last ended.
|
|
199
203
|
|
|
200
204
|
Its record button records the log on the robot with `ros2 bag record`, into a new folder
|
|
201
|
-
(`<recordings folder>/behavior_tree_<date>_<time>` by default),
|
|
202
|
-
page showing the topic shows it recording
|
|
205
|
+
(`<recordings folder>/behavior_tree_<date>_<time>` by default), with the Rosbags widget's
|
|
206
|
+
advanced options, until pressed again: every page showing the topic shows it recording, and
|
|
207
|
+
the Rosbags widgets list it (and may stop it). Its playback button opens a rosbag of the robot
|
|
203
208
|
(a bag's folder, or its `.mcap` or `.db3` file, e.g. of a whole run recorded with
|
|
204
209
|
`ros2 bag record -a`) and plays its log in the widget, on the tree of its file, without
|
|
205
210
|
publishing anything: pause, seek and choose the speed (0.1× to 16×). Bags of Humble and of
|
|
206
|
-
Jazzy are read either way
|
|
211
|
+
Jazzy are read either way, compressed by rosbag2 (zstd, each file or each message) or not;
|
|
212
|
+
SQLite bags need Node.js 22.13, zstd compressed ones 22.15.
|
|
207
213
|
- **Behavior Tree Editor** edits one BehaviorTree of the file at a time (its toolbar switches
|
|
208
214
|
between them, adds new ones, and double clicking a subtree opens its tree, even in an included
|
|
209
215
|
file). Add a node from the palette by dragging it where it goes (into a node, or beside one), or
|
|
@@ -320,6 +326,29 @@ Notes on some of them:
|
|
|
320
326
|
node's section, and the `/**` one) and sets its parameters one by one, saying which the node
|
|
321
327
|
refused and why. Parameter files are read and written by Python's yaml, as `ros2 param` does. A
|
|
322
328
|
widget can be *view only*.
|
|
329
|
+
- **Rosbags** lists the bags of its *folder* (`~/bags` by default; the toolbar browses for
|
|
330
|
+
another): its folders holding a bag, and its lone `.mcap` and `.db3` files, the newest first,
|
|
331
|
+
read again every 2 s while shown. What a bag holds is read from its `metadata.yaml` (Humble's
|
|
332
|
+
and Jazzy's); a bag without it (its recording killed) from its files: MCAP's summary, SQLite's
|
|
333
|
+
tables (up to 64 MB), and it says so (`ros2 bag reindex` makes the metadata). A bag's details
|
|
334
|
+
give its path, its time span, its files, and its topics with their types, counts and rates.
|
|
335
|
+
*Download* sends a folder as a `.tar.gz`, a file as it is; *Delete* deletes bags only, a folder
|
|
336
|
+
holding anything else than a bag's files being refused (the File Manager deletes those).
|
|
337
|
+
*Record* runs `ros2 bag record` into a new folder (`<folder>/rosbag2_<date>_<time>`): the
|
|
338
|
+
topics chosen from the graph (the widget's *topics to record* chosen first, others typed in),
|
|
339
|
+
or all of them, and those matching a regular expression, but those matching another. Its
|
|
340
|
+
*Advanced* options are `ros2 bag record`'s: the storage (the distribution's default, MCAP or
|
|
341
|
+
SQLite, as `ros2 bag list storage` lists them) and its preset (MCAP's `fastwrite`, `zstd_fast`,
|
|
342
|
+
`zstd_small`, SQLite's `resilient`), a new file at a size or a duration (`-b`, `-d`), the
|
|
343
|
+
compression of each file or of each message and its format (`ros2 bag list compressor`: zstd;
|
|
344
|
+
MCAP compresses its chunks with its presets instead, each message is refused), the cache, the
|
|
345
|
+
hidden topics and sim time. The page remembers them for the next recording, the behavior tree
|
|
346
|
+
inspector's too. Recordings show above the bags with their time and size, stopped as Ctrl+C
|
|
347
|
+
stops them, which completes the bag; the server stops them too when it stops. *Play* runs
|
|
348
|
+
`ros2 bag play`, one bag at a time: its messages published again on the robot's topics, at a
|
|
349
|
+
rate, looped, from an offset, some topics only, `/clock` published or not, started paused.
|
|
350
|
+
Pause, seek and the rate go through the player's services (`/rosbag2_player/…`); where it is
|
|
351
|
+
is the server's reckoning from them. A widget can be *view only*.
|
|
323
352
|
- **File Manager** opens its *folder* (`~` by default; `/…`, `~/…`, `package://…`) and lists it
|
|
324
353
|
with its entries' sizes and times, sorted by name (naturally: `run_2` before `run_10`), size or
|
|
325
354
|
time, folders first; the chevrons unfold folders in place. Its places are home, the root, the
|
|
@@ -439,6 +468,21 @@ Notes on some of them:
|
|
|
439
468
|
Without it, the widget says the networks cannot be changed. *Share…* reads the passwords with
|
|
440
469
|
`nmcli --show-secrets`, which needs the same right; those a desktop's keyring keeps cannot be
|
|
441
470
|
read.
|
|
471
|
+
- **System Monitor** reads `/proc` and `/sys` every second, for every page at once, while one
|
|
472
|
+
shows it, and keeps the last 5 minutes for the charts (a page opened later has them at once).
|
|
473
|
+
The temperatures are the kernel's thermal zones and its hwmon chips' sensors (the chips that
|
|
474
|
+
are a thermal zone again, once), grouped by chip with the hottest of each; a sensor is hot from
|
|
475
|
+
its own limits (else 80 °C), and critical at its own critical (else 95 °C). A **Raspberry Pi**'s
|
|
476
|
+
throttling is its firmware's (`get_throttled` in `/sys`, else `vcgencmd get_throttled`): under-
|
|
477
|
+
voltage, frequency capped, throttled, soft temperature limit, now and since boot. A **Jetson**'s
|
|
478
|
+
GPU is devfreq's (its load and clock), its power the INA3221's rails (`VDD_IN` the whole
|
|
479
|
+
board's), its L4T release and power mode (`nvpmodel -q`) are shown with the board. NVIDIA's
|
|
480
|
+
GPUs are read with `nvidia-smi` every 2 s, AMD's from amdgpu's files; an Intel CPU's package
|
|
481
|
+
power from RAPL, if its counters are readable (root's only, since Linux 5.10). Cores taken
|
|
482
|
+
offline (a Jetson's power modes) show as such. The disks are the whole ones (not partitions,
|
|
483
|
+
loop devices, device mapper or RAID), their space the File Manager's; the networks are the
|
|
484
|
+
device's own interfaces (not docker's bridges, VPN clients' tunnels: their traffic is the
|
|
485
|
+
devices' too), or those of its *interfaces* (`wlan0, tun*`). Each section can be hidden.
|
|
442
486
|
|
|
443
487
|
## Plugins
|
|
444
488
|
|
|
@@ -467,16 +511,16 @@ and ship example panels. See **[docs/plugins.md](docs/plugins.md)**.
|
|
|
467
511
|
|
|
468
512
|
Tassello has no login. **Anyone who can open the page can drive the robot, publish on any topic,
|
|
469
513
|
call any service and send or cancel any goal (the ROS Monitor), change any node's parameters
|
|
470
|
-
(Parameters), run any command as
|
|
514
|
+
(Parameters), record, play and delete rosbags (Rosbags), run any command as
|
|
471
515
|
the server's user (the Terminal, a shell), read, write, upload, move and delete any of the
|
|
472
516
|
files that user can (the File Manager), and change the robot's networks and read their passwords
|
|
473
517
|
(Networking)**: keep it on a network you trust, or behind a reverse proxy
|
|
474
518
|
with authentication (it serves everything under `/`, its WebSocket at `/ws`). Changes to the files
|
|
475
519
|
are accepted from the dashboard's own pages only (they send a header that another site's page
|
|
476
520
|
cannot), and the root, the home folder and the system's top folders (`/usr`, `/etc`…) are never
|
|
477
|
-
deleted nor moved. Terminals open,
|
|
478
|
-
those whose origin is the host they reached (the `Host` header, or the
|
|
479
|
-
reverse proxy sets), which another site's page cannot pretend.
|
|
521
|
+
deleted nor moved. Terminals open, networks change and rosbags are deleted from the dashboard's
|
|
522
|
+
own pages only too: those whose origin is the host they reached (the `Host` header, or the
|
|
523
|
+
`X-Forwarded-Host` a reverse proxy sets), which another site's page cannot pretend.
|
|
480
524
|
|
|
481
525
|
## How it works
|
|
482
526
|
|
|
@@ -489,6 +533,8 @@ reverse proxy sets), which another site's page cannot pretend.
|
|
|
489
533
|
├─ bash: the buttons' commands
|
|
490
534
|
├─ python3: the terminals' pseudo-terminals
|
|
491
535
|
├─ nmcli: the networks (NetworkManager)
|
|
536
|
+
├─ ros2 bag: rosbags recorded and played
|
|
537
|
+
├─ /proc, /sys, nvidia-smi: the system monitor
|
|
492
538
|
└─ the plugins' server sides
|
|
493
539
|
```
|
|
494
540
|
|
|
@@ -522,6 +568,13 @@ Safety and bandwidth are the server's concern:
|
|
|
522
568
|
and every 3 s for the signals of the Wi-Fi networks in range, which are scanned for when a page
|
|
523
569
|
opens the widget (at most every 30 s) or asks. Passwords are on `nmcli`'s command line only for
|
|
524
570
|
the moment it takes to save them, activating a connection being another command.
|
|
571
|
+
- Rosbags are recorded and played by `ros2 bag`, the robot's own, as children of the server:
|
|
572
|
+
one recording per folder, for every page (the behavior tree inspectors' among them), one
|
|
573
|
+
playback. A folder of bags is read once for every page showing it, every 2 s, a bag's metadata
|
|
574
|
+
again only when its files change; the recordings' sizes every second.
|
|
575
|
+
- The system monitor's sensors are read once for every page, every second while one watches and
|
|
576
|
+
for 30 s after the last leaves; they are looked for again every 30 s (USB ones come and go).
|
|
577
|
+
Each page gets a sample a second, about 4 KB.
|
|
525
578
|
- The File Manager's folders are read again every 3 s, and answered "unchanged" while they are;
|
|
526
579
|
the largest are listed in part (5,000 entries), and only the rows in sight are drawn. Uploads
|
|
527
580
|
and downloads are streamed, folders archived by `tar` as they are sent; saves replace files at
|
|
@@ -7,7 +7,7 @@ import { BT_RATE } from '../shared/protocol.js';
|
|
|
7
7
|
import { readTree } from './behavior_tree_file.js';
|
|
8
8
|
import { decodeLog, logLayout } from './behavior_tree_log.js';
|
|
9
9
|
import { BehaviorTreeState } from './behavior_tree_state.js';
|
|
10
|
-
import {
|
|
10
|
+
import { openBag, readBag } from './rosbag.js';
|
|
11
11
|
import { absoluteTopic, errorMessage } from './util.js';
|
|
12
12
|
const TICK_MS = 50;
|
|
13
13
|
// How often the page is told where the playback is, while playing
|
|
@@ -18,7 +18,7 @@ const MAX_SPEED = 64;
|
|
|
18
18
|
const MAX_TRANSITIONS = 2_000_000;
|
|
19
19
|
// The bag's log: of the topic if it has it, else its first
|
|
20
20
|
export async function readLog(bag, topic) {
|
|
21
|
-
const logs = await readBag(await
|
|
21
|
+
const logs = await readBag(await openBag(bag), (t) => /\/BehaviorTreeLog$/.test(t.type));
|
|
22
22
|
const found = (topic && logs.get(absoluteTopic(topic))) || [...logs.values()][0];
|
|
23
23
|
if (!found) {
|
|
24
24
|
throw new Error(`No behavior tree log in ${bag}: no nav2_msgs/msg/BehaviorTreeLog topic`);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type rclnodejs from 'rclnodejs';
|
|
2
2
|
import { type BehaviorTreeRecording, type BehaviorTreeUpdate } from '../shared/protocol.ts';
|
|
3
|
+
import { type RecordOptions } from '../shared/rosbag.ts';
|
|
3
4
|
import { type TreeInfo } from './behavior_tree_file.ts';
|
|
4
5
|
import { BehaviorTreePlayback, type PlaybackWatcher } from './behavior_tree_playback.ts';
|
|
5
6
|
import type { StreamState } from './frame_stream.ts';
|
|
7
|
+
import type { Rosbags } from './rosbags.ts';
|
|
6
8
|
import { type Logger } from './util.ts';
|
|
7
9
|
export type RecordingInfo = BehaviorTreeRecording['recording'];
|
|
8
10
|
export interface BehaviorTreeWatcher {
|
|
@@ -17,12 +19,12 @@ export declare class BehaviorTrees {
|
|
|
17
19
|
private readonly uploadsDir;
|
|
18
20
|
private readonly topics;
|
|
19
21
|
private readonly monitors;
|
|
22
|
+
private readonly rosbags;
|
|
20
23
|
private readonly recordings;
|
|
21
|
-
constructor(node: rclnodejs.Node, logger: Logger, uploadsDir: string);
|
|
24
|
+
constructor(node: rclnodejs.Node, logger: Logger, uploadsDir: string, rosbags: Rosbags);
|
|
22
25
|
watch(topic: string, tree: string, watcher: BehaviorTreeWatcher): () => void;
|
|
23
|
-
record(topic: string, path: string | null): Promise<string | null>;
|
|
26
|
+
record(topic: string, path: string | null, options?: RecordOptions): Promise<string | null>;
|
|
24
27
|
play(bag: string, topic: string, tree: string, watcher: PlaybackWatcher): BehaviorTreePlayback;
|
|
25
|
-
stopAll(): Promise<void>;
|
|
26
28
|
private recordingOf;
|
|
27
29
|
private announce;
|
|
28
30
|
private forget;
|
|
@@ -9,17 +9,19 @@
|
|
|
9
9
|
// outlives its last page for LINGER_MS: a page reloaded, or a panel shown
|
|
10
10
|
// again, finds the nodes as they are, which the log only says when they change.
|
|
11
11
|
//
|
|
12
|
-
// A topic is recorded into a rosbag
|
|
13
|
-
//
|
|
14
|
-
//
|
|
12
|
+
// A topic is recorded into a rosbag when a page asks, until one asks to stop
|
|
13
|
+
// (or a Rosbags widget stops it): every page following it is told. The
|
|
14
|
+
// recording is the server's rosbags' (rosbags.ts), as the Rosbags widgets'
|
|
15
|
+
// are. A bag is played back for the page that opens it
|
|
16
|
+
// (behavior_tree_playback.ts).
|
|
15
17
|
import { promises as fs } from 'node:fs';
|
|
16
18
|
import { BT_RATE } from '../shared/protocol.js';
|
|
19
|
+
import { DEFAULT_RECORD_OPTIONS } from '../shared/rosbag.js';
|
|
17
20
|
import { readTree } from './behavior_tree_file.js';
|
|
18
21
|
import { logTransitions } from './behavior_tree_log.js';
|
|
19
22
|
import { BehaviorTreePlayback } from './behavior_tree_playback.js';
|
|
20
23
|
import { BehaviorTreeState } from './behavior_tree_state.js';
|
|
21
24
|
import { publishersQos } from './qos.js';
|
|
22
|
-
import { BagRecorder, newBagFolder } from './rosbag.js';
|
|
23
25
|
import { absoluteTopic, errorMessage } from './util.js';
|
|
24
26
|
// How often the graph is checked for the topic's publishers, until it has one
|
|
25
27
|
const RETRY_PERIOD_MS = 1000;
|
|
@@ -252,12 +254,23 @@ export class BehaviorTrees {
|
|
|
252
254
|
topics = new Map();
|
|
253
255
|
// By topic, then tree source
|
|
254
256
|
monitors = new Map();
|
|
255
|
-
|
|
257
|
+
rosbags;
|
|
258
|
+
// By topic: the folder it is recorded into, '' while it starts
|
|
256
259
|
recordings = new Map();
|
|
257
|
-
constructor(node, logger, uploadsDir) {
|
|
260
|
+
constructor(node, logger, uploadsDir, rosbags) {
|
|
258
261
|
this.node = node;
|
|
259
262
|
this.logger = logger;
|
|
260
263
|
this.uploadsDir = uploadsDir;
|
|
264
|
+
this.rosbags = rosbags;
|
|
265
|
+
// A recording stopped elsewhere, or by itself
|
|
266
|
+
rosbags.onChange(() => {
|
|
267
|
+
for (const [topic, folder] of this.recordings) {
|
|
268
|
+
if (folder && !rosbags.recording(folder)) {
|
|
269
|
+
this.recordings.delete(topic);
|
|
270
|
+
this.announce(topic);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
});
|
|
261
274
|
}
|
|
262
275
|
// Until the returned function is called
|
|
263
276
|
watch(topic, tree, watcher) {
|
|
@@ -290,57 +303,42 @@ export class BehaviorTrees {
|
|
|
290
303
|
}
|
|
291
304
|
// Records the topic into a new folder, or stops (path null): resolves with
|
|
292
305
|
// the bag's folder, null if it was not recording
|
|
293
|
-
async record(topic, path) {
|
|
306
|
+
async record(topic, path, options = DEFAULT_RECORD_OPTIONS) {
|
|
294
307
|
const name = absoluteTopic(topic);
|
|
295
308
|
const current = this.recordings.get(name);
|
|
296
309
|
if (path === null) {
|
|
297
|
-
if (!current
|
|
310
|
+
if (!current) {
|
|
298
311
|
return null;
|
|
299
312
|
}
|
|
300
313
|
this.recordings.delete(name);
|
|
301
314
|
this.announce(name);
|
|
302
|
-
|
|
303
|
-
this.logger.info(`Recorded ${name} into ${current.path}`);
|
|
304
|
-
return current.path;
|
|
315
|
+
return this.rosbags.stop(current);
|
|
305
316
|
}
|
|
306
|
-
if (current) {
|
|
307
|
-
throw new Error(`${name} is being recorded already
|
|
317
|
+
if (current !== undefined) {
|
|
318
|
+
throw new Error(`${name} is being recorded already${current ? `, into ${current}` : ''}`);
|
|
308
319
|
}
|
|
309
|
-
|
|
310
|
-
|
|
320
|
+
this.recordings.set(name, '');
|
|
321
|
+
let folder;
|
|
311
322
|
try {
|
|
312
|
-
|
|
313
|
-
|
|
323
|
+
const request = { path, topics: [name], all: false, regex: '', exclude: '', options };
|
|
324
|
+
folder = (await this.rosbags.record(request, 'behavior-tree')).path;
|
|
314
325
|
}
|
|
315
326
|
catch (e) {
|
|
316
327
|
this.recordings.delete(name);
|
|
317
328
|
throw new Error(`Cannot record ${name}: ${errorMessage(e)}`);
|
|
318
329
|
}
|
|
319
|
-
|
|
320
|
-
recording.recorder.onExit = (reason) => {
|
|
321
|
-
if (this.recordings.get(name) === recording) {
|
|
322
|
-
this.recordings.delete(name);
|
|
323
|
-
this.announce(name);
|
|
324
|
-
this.logger.warn(`Recording ${name} stopped: ${reason}`);
|
|
325
|
-
}
|
|
326
|
-
};
|
|
327
|
-
this.logger.info(`Recording ${name} into ${recording.path}`);
|
|
330
|
+
this.recordings.set(name, folder);
|
|
328
331
|
this.announce(name);
|
|
329
|
-
return
|
|
332
|
+
return folder;
|
|
330
333
|
}
|
|
331
334
|
// A bag played back for a page, until closed
|
|
332
335
|
play(bag, topic, tree, watcher) {
|
|
333
336
|
return new BehaviorTreePlayback({ bag, topic, tree: tree.trim(), uploadsDir: this.uploadsDir, logger: this.logger }, watcher);
|
|
334
337
|
}
|
|
335
|
-
// The recordings completed, as the server stops
|
|
336
|
-
async stopAll() {
|
|
337
|
-
const recorders = [...this.recordings.values()].flatMap((recording) => recording.recorder ?? []);
|
|
338
|
-
this.recordings.clear();
|
|
339
|
-
await Promise.all(recorders.map((recorder) => recorder.stop()));
|
|
340
|
-
}
|
|
341
338
|
recordingOf(topic) {
|
|
342
|
-
const
|
|
343
|
-
|
|
339
|
+
const folder = this.recordings.get(topic);
|
|
340
|
+
const recording = folder ? this.rosbags.recording(folder) : null;
|
|
341
|
+
return recording ? { path: recording.path, since: recording.since } : null;
|
|
344
342
|
}
|
|
345
343
|
announce(topic) {
|
|
346
344
|
for (const monitor of this.monitors.get(topic)?.values() ?? []) {
|
package/dist/server/rosbag.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BagInfo } from '../shared/rosbag.ts';
|
|
1
2
|
export interface BagTopic {
|
|
2
3
|
name: string;
|
|
3
4
|
type: string;
|
|
@@ -10,16 +11,19 @@ export type BagTopics = Map<string, {
|
|
|
10
11
|
topic: BagTopic;
|
|
11
12
|
messages: BagMessage[];
|
|
12
13
|
}>;
|
|
14
|
+
export interface BagSource {
|
|
15
|
+
files: string[];
|
|
16
|
+
messageCompression: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function bagPath(input: string, home?: string): string;
|
|
19
|
+
export declare function openBag(input: string, home?: string): Promise<BagSource>;
|
|
13
20
|
export declare function bagFiles(input: string, home?: string): Promise<string[]>;
|
|
14
|
-
export declare function readBag(
|
|
21
|
+
export declare function readBag(bag: BagSource | string[], accept: (topic: BagTopic) => boolean): Promise<BagTopics>;
|
|
22
|
+
export declare function bagInfo(input: string, home?: string): Promise<BagInfo>;
|
|
23
|
+
export type BagCache = Map<string, {
|
|
24
|
+
version: string;
|
|
25
|
+
info: BagInfo;
|
|
26
|
+
}>;
|
|
27
|
+
export declare function listBags(folder: string, cache?: BagCache, home?: string): Promise<BagInfo[]>;
|
|
15
28
|
export declare function newBagFolder(input: string, home?: string): Promise<string>;
|
|
16
|
-
export declare
|
|
17
|
-
readonly folder: string;
|
|
18
|
-
onExit: ((reason: string) => void) | null;
|
|
19
|
-
private readonly child;
|
|
20
|
-
private output;
|
|
21
|
-
private stopping;
|
|
22
|
-
private constructor();
|
|
23
|
-
static start(folder: string, topics: string[]): Promise<BagRecorder>;
|
|
24
|
-
stop(): Promise<void>;
|
|
25
|
-
}
|
|
29
|
+
export declare function folderSize(folder: string): Promise<number>;
|