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.
Files changed (41) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +62 -9
  3. package/dist/server/behavior_tree_playback.js +2 -2
  4. package/dist/server/behavior_trees.d.ts +5 -3
  5. package/dist/server/behavior_trees.js +33 -35
  6. package/dist/server/rosbag.d.ts +15 -11
  7. package/dist/server/rosbag.js +396 -144
  8. package/dist/server/rosbag_format.d.ts +28 -0
  9. package/dist/server/rosbag_format.js +366 -0
  10. package/dist/server/rosbag_process.d.ts +20 -0
  11. package/dist/server/rosbag_process.js +99 -0
  12. package/dist/server/rosbags.d.ts +42 -0
  13. package/dist/server/rosbags.js +444 -0
  14. package/dist/server/server.js +9 -2
  15. package/dist/server/session.d.ts +4 -0
  16. package/dist/server/session.js +17 -1
  17. package/dist/server/system.d.ts +40 -0
  18. package/dist/server/system.js +606 -0
  19. package/dist/server/system_format.d.ts +51 -0
  20. package/dist/server/system_format.js +264 -0
  21. package/dist/shared/protocol.d.ts +39 -2
  22. package/dist/shared/rosbag.d.ts +104 -0
  23. package/dist/shared/rosbag.js +35 -0
  24. package/dist/shared/system.d.ts +113 -0
  25. package/dist/shared/system.js +45 -0
  26. package/dist/types/client/connection.d.ts +18 -2
  27. package/dist/types/client/icons.d.ts +1 -0
  28. package/dist/types/client/sdk/index.d.ts +3 -1
  29. package/dist/types/shared/protocol.d.ts +39 -2
  30. package/dist/types/shared/rosbag.d.ts +104 -0
  31. package/dist/types/shared/system.d.ts +113 -0
  32. package/dist/www/assets/Joystick.js +1 -1
  33. package/dist/www/assets/RobotModelView.js +1 -1
  34. package/dist/www/assets/TerminalView.js +2 -2
  35. package/dist/www/assets/dialogs.js +1 -1
  36. package/dist/www/assets/icons.js +1 -1
  37. package/dist/www/assets/index.css +1 -1
  38. package/dist/www/assets/index.js +48 -47
  39. package/dist/www/assets/shared/tassello-sdk.js +1 -1
  40. package/docs/plugins.md +4 -1
  41. 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, a file manager, a terminal, and its networks: Wi-Fi
16
- joined, connections switched, the robot as an access point.
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), until pressed again: every
202
- page showing the topic shows it recording. Its playback button opens a rosbag of the robot
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; SQLite bags need Node.js 22.13, zstd compressed MCAP 22.15.
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, and networks change, from the dashboard's own pages only too:
478
- those whose origin is the host they reached (the `Host` header, or the `X-Forwarded-Host` a
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 { bagFiles, readBag } from './rosbag.js';
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 bagFiles(bag), (t) => /\/BehaviorTreeLog$/.test(t.type));
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 (ros2 bag record) when a page asks, until
13
- // one asks to stop: every page following it is told. A bag is played back for
14
- // the page that opens it (behavior_tree_playback.ts).
12
+ // A topic is recorded into a rosbag when a page asks, until one asks to stop
13
+ // (or a Rosbags widget stops it): every page following it is told. The
14
+ // recording is the server's rosbags' (rosbags.ts), as the Rosbags widgets'
15
+ // are. A bag is played back for the page that opens it
16
+ // (behavior_tree_playback.ts).
15
17
  import { promises as fs } from 'node:fs';
16
18
  import { BT_RATE } from '../shared/protocol.js';
19
+ import { DEFAULT_RECORD_OPTIONS } from '../shared/rosbag.js';
17
20
  import { readTree } from './behavior_tree_file.js';
18
21
  import { logTransitions } from './behavior_tree_log.js';
19
22
  import { BehaviorTreePlayback } from './behavior_tree_playback.js';
20
23
  import { BehaviorTreeState } from './behavior_tree_state.js';
21
24
  import { publishersQos } from './qos.js';
22
- import { BagRecorder, newBagFolder } from './rosbag.js';
23
25
  import { absoluteTopic, errorMessage } from './util.js';
24
26
  // How often the graph is checked for the topic's publishers, until it has one
25
27
  const RETRY_PERIOD_MS = 1000;
@@ -252,12 +254,23 @@ export class BehaviorTrees {
252
254
  topics = new Map();
253
255
  // By topic, then tree source
254
256
  monitors = new Map();
255
- // By topic
257
+ rosbags;
258
+ // By topic: the folder it is recorded into, '' while it starts
256
259
  recordings = new Map();
257
- constructor(node, logger, uploadsDir) {
260
+ constructor(node, logger, uploadsDir, rosbags) {
258
261
  this.node = node;
259
262
  this.logger = logger;
260
263
  this.uploadsDir = uploadsDir;
264
+ this.rosbags = rosbags;
265
+ // A recording stopped elsewhere, or by itself
266
+ rosbags.onChange(() => {
267
+ for (const [topic, folder] of this.recordings) {
268
+ if (folder && !rosbags.recording(folder)) {
269
+ this.recordings.delete(topic);
270
+ this.announce(topic);
271
+ }
272
+ }
273
+ });
261
274
  }
262
275
  // Until the returned function is called
263
276
  watch(topic, tree, watcher) {
@@ -290,57 +303,42 @@ export class BehaviorTrees {
290
303
  }
291
304
  // Records the topic into a new folder, or stops (path null): resolves with
292
305
  // the bag's folder, null if it was not recording
293
- async record(topic, path) {
306
+ async record(topic, path, options = DEFAULT_RECORD_OPTIONS) {
294
307
  const name = absoluteTopic(topic);
295
308
  const current = this.recordings.get(name);
296
309
  if (path === null) {
297
- if (!current?.recorder) {
310
+ if (!current) {
298
311
  return null;
299
312
  }
300
313
  this.recordings.delete(name);
301
314
  this.announce(name);
302
- await current.recorder.stop();
303
- this.logger.info(`Recorded ${name} into ${current.path}`);
304
- return current.path;
315
+ return this.rosbags.stop(current);
305
316
  }
306
- if (current) {
307
- throw new Error(`${name} is being recorded already, into ${current.path}`);
317
+ if (current !== undefined) {
318
+ throw new Error(`${name} is being recorded already${current ? `, into ${current}` : ''}`);
308
319
  }
309
- const recording = { path: '', since: 0, recorder: null };
310
- this.recordings.set(name, recording);
320
+ this.recordings.set(name, '');
321
+ let folder;
311
322
  try {
312
- recording.path = await newBagFolder(path);
313
- recording.recorder = await BagRecorder.start(recording.path, [name]);
323
+ const request = { path, topics: [name], all: false, regex: '', exclude: '', options };
324
+ folder = (await this.rosbags.record(request, 'behavior-tree')).path;
314
325
  }
315
326
  catch (e) {
316
327
  this.recordings.delete(name);
317
328
  throw new Error(`Cannot record ${name}: ${errorMessage(e)}`);
318
329
  }
319
- recording.since = Date.now();
320
- recording.recorder.onExit = (reason) => {
321
- if (this.recordings.get(name) === recording) {
322
- this.recordings.delete(name);
323
- this.announce(name);
324
- this.logger.warn(`Recording ${name} stopped: ${reason}`);
325
- }
326
- };
327
- this.logger.info(`Recording ${name} into ${recording.path}`);
330
+ this.recordings.set(name, folder);
328
331
  this.announce(name);
329
- return recording.path;
332
+ return folder;
330
333
  }
331
334
  // A bag played back for a page, until closed
332
335
  play(bag, topic, tree, watcher) {
333
336
  return new BehaviorTreePlayback({ bag, topic, tree: tree.trim(), uploadsDir: this.uploadsDir, logger: this.logger }, watcher);
334
337
  }
335
- // The recordings completed, as the server stops
336
- async stopAll() {
337
- const recorders = [...this.recordings.values()].flatMap((recording) => recording.recorder ?? []);
338
- this.recordings.clear();
339
- await Promise.all(recorders.map((recorder) => recorder.stop()));
340
- }
341
338
  recordingOf(topic) {
342
- const recording = this.recordings.get(topic);
343
- return recording?.recorder ? { path: recording.path, since: recording.since } : null;
339
+ const folder = this.recordings.get(topic);
340
+ const recording = folder ? this.rosbags.recording(folder) : null;
341
+ return recording ? { path: recording.path, since: recording.since } : null;
344
342
  }
345
343
  announce(topic) {
346
344
  for (const monitor of this.monitors.get(topic)?.values() ?? []) {
@@ -1,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(files: string[], accept: (topic: BagTopic) => boolean): Promise<BagTopics>;
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 class BagRecorder {
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>;